如何从指向存储区的指针获取运行时创建的原数组指针?
这个问题确实戳中了C++对象模型里容易混淆的点——尤其是涉及到对象生存期、指针合法性的时候,我来一步步拆解给你理清楚。
首先先明确你的核心场景:你用placement new在一块存储区里创建了一个动态大小的数组,然后通过void*把这块存储区传递出去,现在想把它转回来,得到能安全做数组操作(下标访问、指针算术、用std::span遍历)的指针,同时避免未定义行为(UB)。
先分析你原代码的疑问
你担心process函数里的arr只是指向单个对象,不是数组,所以循环是UB——其实这里有个小误解:
当你用::new (storage) double[count]创建数组时,这个操作会生成两个东西:一个数组对象(类型是double[count]),以及它包含的所有元素。而且根据DR2382的保证,数组对象的起始地址和你传入的storage完全一致,数组第一个元素的地址也和storage一致。
你用std::launder(reinterpret_cast<T*>(storage))得到的arr,本质上和main里的darr是同一个指针——都是指向数组第一个元素的合法指针。而C++标准规定:只要指针指向数组的某个元素,那么针对它的指针算术(比如arr+i)、下标访问(arr[i])都是合法的,只要不越界。所以你原代码里用std::span(arr, arr+n)遍历的操作,其实是合法的,并不是UB。
那为什么会有“只能指向单个对象”的顾虑?可能是混淆了指向数组对象的指针和指向数组元素的指针:前者的类型是T(*)[n](指向大小为n的T数组),后者是T*(指向单个T元素)。但T*只要确实指向数组元素,就具备数组操作的合法性。
标准兼容的正确实现方式
根据不同的C++版本,我们有不同的最优写法:
1. C++17及以后(通用场景,支持所有类型)
如果你已经用placement new创建了数组,那么直接转换并launder就足够了,代码和你原写法一致,但可以更明确地标注意图:
template <typename T> void process(void* storage, std::size_t n) { // 转换为指向数组第一个元素的指针,launder确保我们拿到的是已创建对象的合法指针 T* arr = std::launder(reinterpret_cast<T*>(storage)); // 直接用span包装,安全遍历数组 for (auto& el : std::span(arr, n)) { el = T{2}; } }
这里的关键是:std::launder的作用是告诉编译器,这块存储区里已经有一个有效的T类型对象(数组的第一个元素),所以转换后的指针是合法的,可以安全访问。
2. C++20及以后(隐式生存期类型优化)
如果你的元素类型T是隐式生存期类型(比如基本类型、平凡复制类型、数组等),并且还没创建数组,你可以用std::start_lifetime_as_array直接在存储区创建数组并获取指针,省去placement new:
template <typename T> void process(void* storage, std::size_t n) { // 直接在存储区创建数组并获取第一个元素的指针 T* arr = std::start_lifetime_as_array<T>(storage, n); for (auto& el : std::span(arr, n)) { el = T{2}; } }
但注意:如果已经用placement new创建过数组,就不能再用这个函数——同一个存储区不能存在两个重叠的对象,否则会触发UB。
3. 指向数组对象的指针(编译器扩展,非标准)
如果你确实需要拿到指向数组对象本身的指针(而不是元素),GCC、Clang等编译器支持C风格的变长数组(VLA)扩展,可以这样写:
template <typename T> void process(void* storage, std::size_t n) { // 转换为指向大小为n的T数组的指针 auto arr_obj = std::launder(reinterpret_cast<T(*)[n]>(storage)); // 用*arr_obj访问数组对象,再包装成span for (auto& el : std::span(*arr_obj, n)) { el = T{2}; } }
但这种写法是非标准的,移植性不好,不推荐在跨平台代码中使用。
关于你提到的memmove方案
你想通过std::memmove(storage, storage, sizeof(T)*count)来隐式创建数组,这个思路是有问题的:memmove只是复制字节内容,并不会创建新的对象——原来的数组对象已经存在,这个操作完全多余,甚至可能因为不必要的内存复制引入性能损耗,而且std::launder在这里的作用并没有改变,还是指向单个元素,解决不了你的顾虑。
总结
回到你的场景,最安全、标准兼容的写法就是你原代码的思路:用std::launder把void*转换为T*,然后直接用std::span或指针算术操作数组——只要你确保storage确实是数组第一个元素的地址,且数组已经被正确创建,就不会有UB。
内容来源于stack exchange

