从字节初始化trivially_copyable但不可默认构造对象数组的合规性问题
结论前置
两个方案在C17及更早标准下均属于未定义行为(UB),C20起方案B完全符合标准要求,方案A仍存在合规风险,不推荐使用。
1. 两个方案的实质差异
二者核心逻辑都是直接读取二进制内容到未初始化的存储中再访问,唯一的差异是存储的声明类型:
- 方案A使用
unsigned char[]作为存储,访问时需要强转Foo*,额外存在违反严格别名规则的风险,即使在C++20下该写法的合规性也存在模糊性。 - 方案B直接将
operator new[]返回的存储声明为Foo[]类型,避免了类型别名违规的问题,仅存在「对象未显式创建」的合规风险,这一风险在C++20中已被消除。
2. 不同C++版本的规则差异
C++17及更早标准
所有对象的访问都必须指向显式创建的对象,两个方案都没有调用Foo的构造函数创建实例,本质都是对不存在对象的访问,属于明确的UB。
如果需要在C++17及更早版本下实现完全合规的代码,同时避免无意义的默认构造+覆盖操作,可以用placement new配合字节拷贝实现,对于trivially_copyable类型,编译器会完全优化掉额外的构造逻辑,没有性能损失:
// 申请对齐的存储 std::unique_ptr<unsigned char[]> storage{new unsigned char[dynamicSize * sizeof(Foo)]}; // 读取二进制内容 input.read(reinterpret_cast<char*>(storage.get()), dynamicSize * sizeof(Foo)); // 显式构造Foo实例,trivially_copyable类型下该操作会被优化为空 for (size_t i = 0; i < dynamicSize; ++i) { auto addr = storage.get() + i * sizeof(Foo); new (addr) Foo(*reinterpret_cast<Foo*>(addr)); } // 访问对象 std::cout << reinterpret_cast<Foo*>(storage.get())[index].alpha << "\n"; // 销毁时需要显式调用析构函数 for (size_t i = 0; i < dynamicSize; ++i) { reinterpret_cast<Foo*>(storage.get())[i].~Foo(); }
C++20及更新标准
C20引入了隐式生命周期类型和隐式对象创建规则,trivially_copyable类型属于隐式生命周期类型:当你申请到对齐、大小符合要求的存储后,如果对存储执行了字节拷贝操作(如ifstream::read),编译器会自动隐式创建对应数量的隐式生命周期类型对象,不需要显式调用构造函数。
因此方案B在C20下是完全合规的:::operator new[]返回的存储对齐满足Foo的要求,static_cast<Foo*>转换后执行read操作写入字节时,就自动隐式创建了所有Foo对象,后续访问完全合法,没有任何UB。
3. 推荐实现方案
- 如果目标编译环境仅支持C++17及更早:使用上述placement new的实现方式,性能和原方案一致,完全符合标准。
- 如果目标编译环境支持C++20及以上:直接使用方案B即可,代码简洁且完全合规。
内容的提问来源于stack exchange,提问作者Ad N
相关产品推荐
相关产品推荐

