是否可以以不会导致 UB 的方式分配未初始化的数组?

Posted

技术标签:

【中文标题】是否可以以不会导致 UB 的方式分配未初始化的数组?【英文标题】:Is it possible to allocatate uninialized array in a way that does not result in UB? 【发布时间】:2021-06-11 21:39:18 【问题描述】:

在 C++ 中实现某些数据结构时,需要能够创建一个包含未初始化元素的数组。正因为如此,拥有

buffer = new T[capacity];

不合适,因为new T[capacity] 初始化数组元素,这并不总是可能的(如果 T 没有默认构造函数)或期望的(因为构造对象可能需要时间)。典型的解决方案是分配内存并使用placement new。

为此,如果我们知道元素的数量是已知的(或者至少我们有一个上限)并在堆栈上分配,那么据我所知,可以使用对齐的字节或字符数组,然后使用std::launder 访问成员。

alignas(T) std::byte buffer[capacity];

但是,它只解决了堆栈分配的问题,而没有解决堆分配的问题。为此,我假设需要使用对齐的新,并编写如下内容:

auto memory =  ::operator new(sizeof(T) * capacity, std::align_val_talignof(T));

然后将其转换为 std::byte*unsigned char*T*

// not sure what the right type for reinterpret cast should be
buffer = reinterpret_cast(memory);

但是,有几件事我不清楚。

    如果 ptr 指向与 T 指针可互转换的对象,则定义结果 reinterpret_cast<T*>(ptr)。有关详细信息,请参阅 this answer 或 https://eel.is/c++draft/basic.types#basic.compound-3。我假设,将其转换为 T* 是无效的,因为 T 不一定与 new 的结果指针可互转换。但是,对于char*std::byte,它是否定义良好? 当将new 的结果转换为有效的指针类型(假设它未定义实现)时,它是被视为指向数组第一个元素的指针,还是仅仅指向单个对象的指针?虽然据我所知,它在实践中很少(如果有的话)很重要,但存在语义差异,pointer_type + integer 类型的表达式只有在指向元素是数组成员并且算术结果指向另一个数组元素。 (见https://eel.is/c++draft/expr.add#4)。 就生命周期而言,数组unsigned charstd::byte 类型的对象可以为放置new (https://eel.is/c++draft/basic.memobj#intro.object-3) 的结果提供存储,但是它是否为其他类型的数组定义? 据我所知T::operator newT::operator new[] 表达式在幕后调用::operator new::operator new[]。由于内置new 的结果为void,如何转换为正确的类型?这些实现是基于还是我们有明确的规则来处理这些? 释放内存时,应该使用
::operator delete(static_cast<void*>(buffer), sizeof(T) * capacity, std::align_val_talignof(T));

还是有别的办法?

PS:我可能会在实际代码中将标准库用于这些目的,但我会尝试了解幕后的工作原理。

谢谢。

【问题讨论】:

"new T[] 初始化数组元素" 不,它没有。 new T[]() 会,但不会new T[]。我的意思是,它将默认初始化它们,因此如果存在默认构造函数,它将被调用。但是如果T 是一个普通类型,它将保持未初始化状态。那么这里的“未初始化”到底是什么意思?您的意思是没有实际的Ts,还是您希望Ts 存在但具有未初始化的值? 我有兴趣为 T 的实例留出空间而不构建它们。由于它们稍后可能会被破坏,因此“没有实际的 T”是正确的术语。我更正了new T 声明。 【参考方案1】:

指针互转换

关于指针互转换,使用T *[unsigned] char|std::byte * 无关紧要。您必须将其转换为 T * 才能使用它。

请注意,您必须调用std::launder(在转换结果上)来访问指向的T 对象。唯一的例外是创建对象的placement-new 调用,因为它们还不存在。手动析构函数调用不是例外。

如果您不使用std::launder,那么缺乏指针互转换性只会成为问题。

当将 new 的结果转换为有效的指针类型时(假设它不是实现定义的),它是被视为指向数组第一个元素的指针,还是只是指向单个对象的指针?

如果您想更加安全,请在执行任何指针运算后将指针存储为[unsigned] char|std::byte *reinterpret_cast

数组unsigned charstd::byte类型的对象可以为放置new的结果提供存储

该标准在任何地方都没有说明放置新功能需要“提供存储空间”才能工作。我认为该术语的定义仅用于标准中其他术语的定义。

考虑[basic.life]/example-2,其中operator= 使用placement-new 就地重构对象,即使类型T 没有为同一类型T“提供存储”。

既然 builtin new 的结果是 void,那么如何转换成正确的类型呢?

不确定标准对此有何规定,但除了reinterpret_cast 之外还能是什么?

释放内存

你的方法看起来是正确的,但我think你没有传递大小。

【讨论】:

我同意我在结果上需要std::launder,但理论点是指针算术。 虽然我不确定我是否正确解释了 eel.is/c++draft/intro.object#10>,但是 new(... ) 隐式构造了一个 bytechar 数组,而它没有隐式构造一个数组的T。至于“提供存储”,我的措辞令人困惑。我的意思是,使用placement new 或显式销毁不会“弄乱”无符号整数数组,而可能会“弄乱”T 数组。 在调用(TArray + i)-&gt;~T()之后,我有一个无效状态的数组,它不再是一个数组,因此理论上没有定义指针算法,并且可能没有定义放置新位置的计算。 @RazielMagius 这就是为什么我建议在 char * 上进行算术运算。 @RazielMagius 是的,它也应该适用于非过度对齐的类型。 “你说...是什么意思”我说没有办法使用它。在不指定大小的情况下如何分配内存?【参考方案2】:

我认为你的前提可能不正确。如果 T 是一个类,则应调用默认构造函数。但是,这可以是空白的,如果您的类包含所有 POD(普通旧数据),则不会初始化任何内容。实际上,我一直都依赖它,因为出于性能原因,我经常不希望初始化。

我相信对于全局数据等有一些注意事项,其中有些东西是零初始化的。但总的来说,堆的东西不是。你可以测试一下,你会发现内存中有一堆垃圾,至少在发布模式下编译时是这样。一些编译器会在调试模式下初始化内存,但这是在构造函数之外完成的。

例如,您可以在自定义放置新函数中设置数据,如果它是 POD,它仍将存在于构造函数中。有些人会争辩说这是 UB,但我认为标准对 POD 说“什么都不做”,这意味着没有初始化。

【讨论】:

以上是关于是否可以以不会导致 UB 的方式分配未初始化的数组?的主要内容,如果未能解决你的问题,请参考以下文章

负数的按位运算会导致 ub 吗?

UB 是不是将 void 指针与 C 中的类型化指针进行比较(是不是相等)?

是否可以在不重新分配的情况下实现动态数组?

keil5数组下标异常

es 基于磁盘的shard分配参数

以下链式赋值是不是会导致未定义的行为?