如何创建一个数组并在不开始其任何元素的生命周期的情况下开始其生命周期?

How to create an array and start its lifetime without starting the lifetime of any of its elements?

任何类型的数组都是implicit-lifetime objects, and it is possible to to begin the lifetime of implicit-lifetime object, without beginning the lifetime of its subobjects

据我所知,在不开始其元素生命周期的情况下以不导致 UB 的方式创建数组的可能性是隐式生命周期对象的动机之一,请参阅 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p0593r6.html.

现在,正确的做法是什么?分配内存并返回指向数组的指针就足够了吗?或者还有其他需要注意的地方?

也就是说,这段代码是否有效,它是否创建了一个包含未初始化成员的数组,或者我们仍然有 UB?

// implicitly creates an array of size n and returns a pointer to it
auto arrPtr = reinterpret_cast<T(*)[]>(::operator new(sizeof(T) * n, std::alignval_t{alignof(T)}) );
// is there a difference between reinterpret_cast<T(*)[]> and reinterpret_cast<T(*)[n]>?
auto arr = *arrPtr; // de-reference of the result in previous line.

问题可以重述如下。

根据https://en.cppreference.com/w/cpp/memory/allocator/allocateallocate 函数函数在存储中创建一个T[n]类型的数组并开始其生命周期,但是不会开始其任何元素的生命周期。

一个简单的问题 - 它是如何完成的? (忽略 constexpr 部分,但如果 constexpr 部分也在答案中进行了解释,我不介意)。

PS:提供的代码对 c++20 有效(假设它是正确的),但据我所知对早期标准无效。

我相信这个问题的答案也应该回答我之前提出的两个类似问题。

  1. 数组和隐式生命周期对象 创作.
  2. 是否可以分配未初始化的数组 不会导致 UB.

编辑:我添加了一些代码片段,以使我的问题更清楚。如果能解释哪些有效哪些无效,我将不胜感激。

PS:随意将 malloc 替换为对齐版本,或 ::operator new 变体。据我所知,这并不重要。

示例 #1

T* allocate_array(std::size_t n)
{
    return reinterpret_cast<T*>( malloc(sizeof(T) * n) ); 
    // does it return an implicitly constructed array (as long as 
    // subsequent usage is valid) or a T* pointer that does not "point"
    // to a T object that was constructed, hence UB
    // Edit: if we take n = 1 in this example, and T is not implicit-lifetime 
    // type, then we have a pointer to an object that has not yet been
    // constructed and and doesn't have implicit lifetime - which is bad
}

示例 #2。

T* allocate_array(std::size_t n)
{
    // malloc implicitly constructs - reinterpet_cast should a pointer to 
    // suitably created object (a T array), hence, no UB here. 
    T(*)[] array_pointer = reinterpret_cast<T(*)[]>(malloc(sizeof(T) * n) );
    // The pointer in the previous line is a pointer to valid array, de-reference
    // is supposed to give me that array
    T* array = *array_pointer;
    return array;
}

示例 #3 - 与 2 相同,但数组大小已知。

T* allocate_array(std::size_t n)
{
    // malloc implicitly constructs - reinterpet_cast should a pointer to 
    // suitably created object (a T array), hence, no UB here. 
    T(*)[n] n_array_pointer = reinterpret_cast<T(*)[n]>(malloc(sizeof(T) * n) );
    // The pointer in the previous line is a pointer to valid array, de-reference
    // is supposed to give me that array
    T* n_array = *n_array_pointer;
    return n_array;
}

这些有效吗?


答案

虽然标准的措辞不是 100% 清楚,但在更仔细地阅读论文后,动机是使转换为 T* 合法而不是转换为 T(*)[]Dynamic construction of arrays. Also, the changes to the standard by the authors of the paper imply that the cast should be to T* and not to T(*)[]. Hence, the accepting 作为我问题的正确答案。

隐式对象创建的全部意义在于它是隐式。也就是说,您不做任何事情来让它发生。一旦 IOC 发生在一块内存上,您就可以使用该内存,就好像所讨论的对象存在一样,只要您这样做,您的代码就可以工作。

当您从 allocator_traits<>::allocate 取回 T* 时,如果您将指针加 1,则该函数 return 编辑了一个至少包含 1 个元素的数组(新的指针可以是数组的尾指针)。如果再次加 1,则函数 returned 了一个至少包含 2 个元素的数组。等等 None 这是未定义的行为。

如果你做了一些与此不一致的事情(转换为不同的指针类型并表现得好像那里有一个数组),或者如果你表现得好像数组超出了 IOC 适用的存储大小,然后你得到UB。

所以 allocator_traits::allocate 实际上不需要做任何事情,只要分配器隐式分配的内存创建对象即可。


// does it return an implicitly constructed array (as long as 
// subsequent usage is valid) or a T* pointer that does not "point"
// to a T object that notconstructed, hence UB

都没有。它 return 是一个指针(指向类型 T),指向可能已经隐式创建了对象的存储。 隐式创建了哪些 个对象取决于您如何使用此存储。仅仅进行转换并不构成“使用”存储空间。

导致 UB 的不是 reinterpret_cast;问题是 return 被不当的 reinterpret_cast 编辑了。由于 IOC 的工作基于可能导致 UB 的操作,IOC 不关心你将指针指向什么。

IOC 规则的重要组成部分是推论“suitable created object" rule. This rule says that certain operations (like malloc and operator new) return 一个指向“合适的创建对象”的指针。本质上它回到了量子叠加:如果 IOC 追溯创建一个对象为了使您的代码正常工作,这些函数会追溯 return 指向使您的代码正常工作的任何对象的指针。

因此,如果您的代码将指针用作 T* 并对该指针进行指针运算,则 malloc return 将指针指向 [= 数组的第一个元素=14=]秒。那个数组有多大?这取决于:分配有多大,以及你的指针运算有多远?里面有活的 T 吗?这取决于:您是否尝试访问数组中的任何 Ts?