C++17及以上标准中,单成员union数组可转为元素数组吗?
关于C++17中MaybeUninit数组转换为T数组的合法性问题
问题描述
我正在实现一个运行时固定大小的数组类,功能类似Rust的Box<[T]>——因为初始化后大小不再改变,所以可以省去容量跟踪的额外空间。为了支持无默认构造函数的类型,我允许用户传入一个接收索引并生成T的生成函数。
为了解耦内存分配与初始化,我参考了CppCon 2019上CJ Johnson的演讲《How to Hold a T》,先创建一个单成员union MaybeUninit<T>的数组:
union MaybeUninit<T> { char dummy; T val; MaybeUninit() : dummy() {} ~MaybeUninit() {} };
在完成所有元素的val成员初始化后,我有两个疑问:
- 依据C++17及以上标准,是否允许使用
reinterpret_cast<T*>(m_array)将其视为T类型的数组并直接解引用? - 如果不允许,有没有无需拷贝的方式将
MaybeUninit<T>*转换为T*?
回答
一、直接reinterpret_cast的合法性问题
直接用reinterpret_cast<T*>(m_array)并将其当作T数组解引用,不符合C++17及以后的标准,会触发未定义行为,核心原因有两点:
- 违反对象别名规则:你创建的是
MaybeUninit<T>类型的数组,每个元素都是MaybeUninit<T>对象,而非T对象。即使初始化了内部的val成员,也不能将整个数组的指针跨类型重新解释为T数组指针。 - 数组类型不可随意替换:C++标准明确数组的元素类型是固定的,不能通过指针强制转换改变数组的元素类型,哪怕内存布局看似兼容。
二、无需拷贝的合规转换方式
要合法获取T数组的指针且不触发拷贝,可以采用以下两种标准允许的方式:
1. 基于单个元素地址的连续视图
由于MaybeUninit<T>是union类型,其首地址与内部val成员的地址完全重合,且MaybeUninit<T>数组是连续存储的,因此每个val成员的地址必然连续。可以直接取第一个元素的val地址作为T数组的起始指针:
// 假设m_array是MaybeUninit<T>[]类型,size为数组长度 T* get_data() { // 这里的reinterpret_cast仅转换单个元素的地址,符合标准 return reinterpret_cast<T*>(&m_array[0].val); }
这种方式本质是直接访问已初始化的T对象,而非重新解释整个数组的类型,因此是合规的。
2. 使用std::launder(C++17及以上)
如果需要明确告知编译器地址上的T对象已完成初始化,可以用std::launder来获取合法的T指针:
#include <memory> T* get_data() { // std::launder确保编译器识别地址上的T对象已初始化 T* first = std::launder(&m_array[0].val); // 因存储连续,指针算术可合法访问后续元素 return first; }
std::launder的作用是解决"对象被放置在已存在的存储中"时的类型别名问题,确保后续的指针访问和算术操作符合标准。
三、额外注意事项
- 必须确保所有
MaybeUninit<T>的val成员都已完全初始化,否则访问T对象会触发未定义行为。 - 销毁数组时,不能直接用
delete[]销毁T*指针,需逐个调用T的析构函数,再销毁MaybeUninit<T>数组。
内容的提问来源于stack exchange,提问作者user17789309
相关产品推荐
相关产品推荐

