通过底层分配函数获取数组的正确方式
通过底层分配函数获取数组的正确方式
咱们先从基础说起,大部分底层内存分配函数返回的都是void *类型指针。最近我在琢磨一个问题:如果我想在分配到的这块内存上做指针运算,或者直接用它来分配一个数组,而且已知T是隐式生存期类型的话,下面这段写法是不是能避开未定义行为的正确操作?
template <typename T> auto* make_array(std::size_t N) { return std::launder(reinterpret_cast<T*>( ::operator new(N * sizeof(T), std::align_val_t(alignof(T))) )); }
其实这个写法在符合条件的前提下是安全的,咱们拆解开来看细节:
- 首先,
::operator new分配了足够容纳N个T对象的内存,并且通过std::align_val_t(alignof(T))指定了和T匹配的对齐要求,这一步直接保证了内存的大小和对齐都是完全合法的,不会出现对齐违规的问题。 - 接着用
reinterpret_cast<T*>把void*转换成T*类型,这里因为T是隐式生存期类型,它不需要显式构造就能合法使用对应的内存空间(比如int、char这类基本类型,或者简单的聚合类型都属于这类),所以转换后的指针指向的内存是完全符合T的布局要求的。 - 最后调用
std::launder是很关键的一步:如果这块内存之前被用于存储其他类型的对象,std::launder能确保我们拿到的指针是指向新的T数组元素的有效指针,彻底避开因为对象生存期重叠可能引发的未定义行为。
另外还有几个必须注意的点,不然很容易踩坑:
- 这个写法只适用于
T是隐式生存期类型的情况,如果是非隐式生存期类型,必须显式调用构造函数初始化对象,不然直接用指针操作就是未定义行为。 - 关于指针运算:既然我们已经拿到了指向数组首元素的有效
T*指针,那么在[0, N)范围内的指针偏移运算都是完全合法的,不会触发未定义行为。 - 内存释放要配对:用完这块内存后,一定要用
::operator delete来释放,并且同样要指定对应的对齐值,保证分配和释放的操作完全匹配。
内容来源于stack exchange
相关产品推荐
相关产品推荐

