如何在C++20中无未定义行为地模拟std::start_lifetime_as_array?
如何在C++20中无未定义行为地模拟std::start_lifetime_as_array?
正如你提到的,目前还没有编译器支持std::start_lifetime_as_array,而之前已经有了模拟单个对象版本std::start_lifetime_as的实现思路。要模拟数组版本,我们得先吃透标准里对这个函数的定义,再结合C++20的规则来实现,同时避开未定义行为。
首先回顾cppreference对std::start_lifetime_as_array的核心说明:当n > 0时,它等价于对“n个T的数组”类型调用std::start_lifetime_as,但问题在于n是运行时才确定的——我们没法把动态大小的数组类型直接作为模板参数传给模拟的单个版本函数。不过我们可以利用C++中隐式创建对象的规则来绕开这个限制。
和单个元素的模拟版本一样,我们需要对T添加必要约束:它必须是平凡可复制类型(std::is_trivially_copyable_v<T>)且隐式生命周期类型(std::is_implicit_lifetime_v<T>)。只有满足这两个条件,我们才能安全地隐式创建数组,且内存操作不会触发未定义行为。
以下是符合要求的模拟实现:
#include <cstring> #include <memory> #include <type_traits> template<class T> requires (std::is_trivially_copyable_v<T> && std::is_implicit_lifetime_v<T>) T* start_lifetime_as_array(void* p, std::size_t n) noexcept { if (n == 0) { // n=0时不创建任何对象,返回的指针可作为尾后指针使用(匹配标准函数行为) return static_cast<T*>(p); } // 用memmove覆盖整个数组内存,触发隐式创建n个T的数组 std::memmove(p, p, n * sizeof(T)); // launder告诉编译器:当前指针指向的是新隐式创建的数组对象 return std::launder(static_cast<T*>(p)); }
为什么这个实现是安全的?
我们来拆解每一步的合法性:
- 约束条件:
std::is_trivially_copyable_v<T>保证memmove操作对T的内存是安全的;std::is_implicit_lifetime_v<T>确保T类型的对象可以被隐式创建在满足对齐和大小要求的内存中——这两个前提是避免UB的基础。 - n=0的分支:直接返回转换后的指针,这符合标准函数的设计:此时不会创建任何对象,返回的指针可作为数组的尾后指针使用(支持和首元素指针比较等合法操作)。
- n>0的分支:
std::memmove(p, p, n * sizeof(T)):看似是无意义的自复制,但这个操作会触发C++的隐式对象创建规则——我们正在以“n个T的数组”的内存布局访问这块内存,而T是隐式生命周期类型,编译器会认可这里隐式创建了对应的数组对象。std::launder:必须调用这个函数来告知编译器,当前指针指向的是新创建的数组对象,而非原内存中可能存在的其他对象。这一步能确保后续对返回指针的数组访问完全合法,不会触发UB。
最后要注意:你需要确保传入的p指向的内存满足T的对齐要求,且内存大小至少为n * sizeof(T)——这和标准std::start_lifetime_as_array的要求完全一致,如果不满足这些前提,不管是标准函数还是我们的模拟版本都会触发未定义行为。
备注:内容来源于stack exchange,提问作者Alex O
相关产品推荐
相关产品推荐

