【问题标题】:Alignment of array with 0 elements数组与 0 个元素对齐
【发布时间】:2018-05-30 08:31:44
【问题描述】:

C++allows dynamic allocation of zero-sized arrays:

int* p = new int[0];
delete[] p;

我不能用这样的指针做很多事情(因为数组没有元素),但是需要新的表达式来给我一个有效的 (!= nullptr) 指针,然后我必须再次 delete[] 作为如果它是一个实际的数组。

对于这种新表达式返回的内存的对齐方式有什么要求吗?考虑:

struct alignas(8) Foo {
    int x;
};

Foo* p = new Foo[0];
delete[] p;

p 是否保证指向一个 8 位对齐的地址?此外,如果我编写自定义分配器,在这种情况下是否需要返回指向对齐地址的指针?

【问题讨论】:

  • 因为取消引用指针是 UB 有关系吗?
  • @RichardCritten 我真的不知道。这就是我要问的部分原因。

标签: c++ c++11 language-lawyer allocator


【解决方案1】:

N3337 的basic.stc.dynamic.allocation/2(基本上是C++11):

分配函数尝试分配请求的数量 贮存。成功则返回起始地址 一个存储块,其字节长度至少应该一样大 作为要求的大小。内容没有限制 从分配函数返回时分配的存储空间。命令, 连续性和连续调用分配的存储的初始值 分配函数是未指定的。 返回的指针应 适当对齐,以便它可以转换为任何指针 具有基本对齐要求的完整对象类型 (3.11) 然后用于访问分配的存储中的对象或数组 (直到通过调用 相应的释放函数)。即使空间大小 请求为零,请求可能失败。如果请求成功,则 返回的值应为非空指针值 (4.10) p0 不同 从任何先前返回的值 p1,除非该值 p1 是 随后传递给操作员删除。取消引用的影响 作为零大小请求返回的指针未定义。

基本对齐 (basic.align/2):

基本对齐由小于或的对齐表示 等于实现支持的最大对齐 所有上下文,等于 alignof(std::max_align_t)

扩展对齐(basic.align/3):

扩展对齐由大于的对齐表示 alignof(std::max_align_t)。

是否有任何扩展对齐是由实现定义的 支持以及支持它们的上下文

因此,operator new 返回的指针必须具有基本对齐。即使指定了零大小。它是实现定义的,无论 8 是基本对齐还是扩展对齐。如果它是基本的,那么Foo 是可以的。如果它被扩展,那么它是实现定义Foo支持operator new

注意,对于 C++17,情况有所改善:


basic.stc.dynamic.allocation/2 of C++17:

分配函数尝试分配请求的数量 贮存。成功则返回起始地址 一个存储块,其字节长度至少应该一样大 作为要求的大小。内容没有限制 从分配函数返回时分配的存储空间。命令, 连续性和连续调用分配的存储的初始值 分配函数是未指定的。 返回的指针应 适当对齐,以便可以将其转换为指向任何 合适的完整对象类型([new.delete.single]),然后用于 访问分配的存储中的对象或数组(直到存储 通过调用相应的解除分配显式解除分配 功能)。即使请求的空间大小为零, 请求可能会失败。如果请求成功,返回值应为 一个非空指针值 ([conv.ptr]) p0 不同于以前的任何 返回值 p1,除非该值 p1 随后被传递给 运算符删除。此外,对于库分配函数 [new.delete.single] 和 [new.delete.array],p0 代表 与任何其他存储不相交的存储块的地址 调用者可访问的对象。通过一个间接的效果 作为零大小请求返回的指针未定义。

我已经把重点放在了相关部分。那句话的意思是void *operator new(...)的返回指针应该有合适的对齐方式。它没有提到零大小作为特例(但是,当然,取消引用返回的指针是 UB)。

所以答案是平常的,没有对零的特殊处理:

  1. void *operator new(std::size_t) 必须返回 alignof(std​::​max_­align_­t) 的对齐指针
  2. void *operator new(std::size_t, std::align_val_t align) 必须返回一个对齐的指针 align)

请注意,它是实现定义的,将为Foo 调用哪个版本。这取决于 8 是否等于或小于alignof(std​::​max_­align_­t)。如果小于,则调用第一个版本(因为它没有扩展对齐)。否则调用第二个。


更新:作为 Massimiliano Janes cmets,这些段落适用于 operator new 的结果,而不适用于新表达式的结果。实现可以向operator new[] 的结果添加任意偏移量。并且标准对这个x 偏移量的值保持沉默:

new T[5] 会导致以下调用之一:

算子 new[](sizeof(T) * 5 + x)

运算符 new[](sizeof(T) * 5 + x, std::align_val_t(alignof(T)))

这里,x 的每个实例都是一个非负的未指定值 表示数组分配开销;的结果 new-expression 将从返回的值偏移这个量 通过运算符 new[]。此开销可应用于所有阵列 新表达式,包括那些引用库函数的表达式 operator new[](std​::​size_t, void*) 和其他布局分配 职能。开销的数量可能与一次调用 new 不同 到另一个。

但是,在我看来,这个x 偏移量不能是任意的。如果它不是对齐的倍数,那么新表达式将返回一个非对齐指针(在所有情况下。不仅是零,还有非零大小参数)。这显然不是我们想要的。

所以我认为这是标准中的一个漏洞。 x 的值应限制为对齐的倍数(至少在非零分配情况下)。但是由于这个遗漏,标准似乎不能保证new[] 表达式完全返回对齐的指针(在非零情况下也是如此)。

【讨论】:

  • 遗憾的是,这也不能回答问题;首先,new 表达式的结果可能会与数组情况下的分配结果任意偏移(参见[expr.new#15]),因此它不能证明 new 表达式 的结果与零大小的数组大小写。
  • 其次,尚不清楚用于表示空数组的非对齐指针是否“适当对齐,以便可以将其转换为指针......”或不是......唯一的事情我们知道(来自[basic.compound#3])是一个非对齐指针是一个无效的指针值,但是没有地方说delete[]需要一个有效的指针值,只是说它需要上一个 new[]... 的结果
  • @MassimilianoJanes:首先关心的是:为什么零大小的数组在这里很特别?第二:如果允许无效的指针值,那么这句话就没有任何意义。我认为“可以转换”意味着它不会以无效值结束。
  • @MassimilianoJanes:您似乎对此有一个有效的观点。然而,有些事情对我来说很奇怪。 x 偏移量应该是对齐的倍数,不是吗(在一般情况下)?如果不是,那么新表达式将返回一个对齐错误的指针。但是,当然,实现可以选择 x 可以是 0 大小情况下的任何内容。我会稍微修改一下我的答案,谢谢你的信息!
  • 我认为整个问题归结为一个问题:always 所需的新表达式和分配函数是否会产生 valid 指针值(即使在零数组情况下)?如果是,则 [basic.compound#3] 应用并且非空有效指针必须始终对齐(独立于它们指向的对象,如果有的话)。正如你一样,我在这两种情况下都倾向于积极,但我不知道...... :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-20
  • 2014-12-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多