在C++中为零大小的分配返回唯一地址背后的基本原理是什么?



在C++中为零大小的分配返回唯一地址背后的基本原理是什么?

背景:C11标准关于malloc(7.20.3内存管理功能)的说法:

如果请求的空间大小为零,则行为是实现定义的:返回 null 指针,或者行为就像大小是某个非零值,只是返回的指针不应用于访问对象。

也就是说,正如我所看到的,malloc总是成功的零大小分配,因为您唯一可以使用零大小分配的指针来调用其他一些内存分配函数,例如使用它free

  • 如果malloc返回NULLfree(NULL)就可以了,所以这可以被认为是成功的,
  • 如果它返回其他一些值,那也是成功的(因为它不是NULL),唯一的条件是free值也应该有效。

此外,C11(也是 7.20.3)没有指定从 malloc 返回的地址必须是唯一的,只是它们必须指向不相交的内存区域:

如果分配成功,则返回的指针将适当对齐,以便可以将其分配给指向任何类型的对象的指针,然后用于访问分配空间中的此类对象或此类对象的数组(直到空间被显式解除分配)。已分配对象的生存期延长 从分配到解除分配。每次此类分配都应产生一个指向与任何其他对象不相交的对象的指针。

所有大小为零的对象都是不相交的AFAICT,这意味着malloc可以为多个零大小分配返回相同的指针(例如NULL会很好),或者每次都有不同的指针,或者某些指针相同,等等。

然后 C++98 附带了两个原始内存分配函数:

void* operator new(std::size_t size);
void* operator new(std::size_t size, std::align_val_t alignment);

请注意,这些函数仅返回原始内存:它们不会创建或初始化任何类型的 AFAICT 的任何对象。

你这样称呼它们:

#include <iostream>
#include <new>
int main() {
void* ptr = operator new(std::size_t{0});
std::cout << ptr << std::endl;
operator delete(ptr, std::size_t{0});
return 0;
}

C++17 标准的[new.delete.single]部分解释了它们,但我认为它的关键保证是在[basic.stc.dynamic.allocation]中给出的:

即使请求的空间大小为零,请求也可能失败。如果请求成功,则返回的值应为非空指针值 (7.11) p0,不同于任何先前返回的值 p1,除非该值 p1 随后传递给运算符 delete。此外,对于 21.6.2.1 和 21.6.2.2 中的库分配函数,p0 应表示与调用方可访问的任何其他对象的存储不相交的存储块的地址。通过作为零大小请求返回的指针进行间接处理的效果是未定义的。38

也就是说,它们必须始终返回不同的成功指针。这比malloc有点变化。

我的问题是:这种变化背后的理由是什么?(也就是说,在C++中为零大小的分配返回唯一地址的背后)

理想情况下,答案只是指向探索替代方案并激发其语义的论文(或其他来源)的链接。通常,对于这些C++98问题,我会选择C++的设计与演变,但第10节(内存管理)没有提到任何关于它的内容。否则,某种权威的参考会很好。


免责声明:我在reddit上问过它,但我问得不够好,所以我没有得到任何有用的答案。我想恳请您,如果您只有一个假设,请随时将其作为答案发布,但要提及它只是一个假设。

此外,在reddit上,人们一直在谈论零大小的类型,我是否有改变标准的建议,等等。这个问题是关于原始内存分配函数在传递大小等于零时的语义。如果像零大小类型这样的主题与您的答案相关,请包括它们!但尽量不要因无关紧要的问题而脱轨。

此外,在reddit上,人们还抛出了诸如"这是出于优化目的"之类的论点,而实际上却无法提及更具体的内容。我希望答案中比"因为优化"更具体的东西。例如,一个redditor提到了别名优化,但我想知道哪种别名优化适用于无法取消引用的指针,并且无法让任何人对此发表评论。因此,也许如果您要提及优化,那么一个小示例可以显示它将丰富讨论。

问题是C++中的对象(无论其大小)必须具有唯一的标识。因此,不同的共存对象(无论其大小如何)必须具有不同的地址,因为假定两个比较相等的指针指向同一对象

如果你承认零大小的对象可以有相同的地址,你就无法再区分两个地址是不是同一个对象。


关于"new 不返回对象"问题的许多评论。

在这种情况下,请忘记 OOP 术语:

C++规范对"对象"一词的含义有精确的定义。

CPP 参考:对象

特别:

C++程序创建、销毁、引用、访问和操作对象。 在C++中,对象是一个存储区域,它具有

大小
  • (可以用大小确定);
  • 对齐
  • 要求(可以通过对齐确定);
  • 存储持续时间(自动、静态、动态、线程本地);
  • 生命周期(以存储持续时间或临时为限);
  • 类型;
  • 值(可以是不确定的,例如对于默认初始化的非类类型);
  • (可选)名称。

以下实体不是对象:值、引用、函数、 枚举器、类型、非静态类成员、位字段、模板、类或 函数模板专用化、命名空间、参数包等。

变量是不是非静态数据成员的对象或引用, 这是通过一项宣言引入的。

对象是通过定义、新表达式、抛出表达式创建的,当 更改联合的活动成员,以及临时对象的位置 必填。

原因很简单,代码不应该要求对边界条件进行特殊处理。许多算法,我想说大多数,必须将零大小的对象作为边界条件来处理。不太常见的是将指针与对象进行比较以查看它们是否是同一对象的算法,但即使对于零大小的对象,这仍然应该有效。

但是,您的问题假设这是一个更改。除了 1980 年代后期的短暂中断之外,我所知道的所有 C 和 C++ 实现一直都是这样的。

dmr 的原始 C 编译器的行为是这样的,但在 1987 年左右,C 标准草案指定零大小对象的 malloc 返回 NULL。这真的很奇怪,甚至最终的 C-89 标准也让它实现了定义,但从那以后我从未遇到过做这种可怕事情的实现。

我在博客的"Malloc Madness"部分对此进行了更多讨论。

最新更新