You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

C++ placement new后reinterpret_cast的严格别名与UB问题问询

C++中placement new配合reinterpret_cast的未定义行为与容器实现疑问

问题背景

以下代码按C标准属于未定义行为(UB),但许多类数组容器(如std::vector)却采用类似方式实现:先使用placement new创建对象,再通过reinterpret_cast<T*>转换指针。但根据C严格别名规则,这属于UB。

相关疑问点:

  • p0532r0指出std::launder()无法解决该问题;
  • std::start_lifetime_as()似乎也无法解决该问题;
  • p0593r6能否解决此问题?

代码示例

struct T { int x; };
alignas(T) std::byte buffer[sizeof(T)];
// 1. 使用placement new创建对象
new(buffer) T{42};       // #obj1
// 问题:如何通过buffer指针获取#obj1?
T *t = reinterpret_cast<T*>(buffer);
auto obj1 = t[0];             // UB:T与std::byte不别名(严格别名规则);

核心问题

  1. C++标准中哪条条款可使上述代码成为定义行为?
  2. 若不可行,std::vector类容器是如何正确实现此类操作的?

问题1:标准中是否有条款让代码成为定义行为?

目前C标准(包括C23)里,没有直接条款能让示例中的代码成为定义行为。严格别名规则明确规定:除非是char/unsigned char/std::byte类型的指针访问其他类型对象(反过来不行),否则通过不同类型指针访问对象属于UB。示例中用T*访问原本是std::byte数组的内存,哪怕里面已经用placement new构造了T对象,也违反规则——因为std::byte数组的生命周期还没结束,编译器可能认定buffer指向的还是std::byte数组,而非T对象,进而做出错误优化。

针对你提到的几个提案:

  • p0532r0:明确了std::launder无法解决这种“原数组对象仍存活,新对象在其内存上构造”的场景;
  • std::start_lifetime_as<T>(buffer):它的作用是将内存视为T类型对象的起始,但前提是内存没有关联其他存活对象——示例中std::byte数组还活着,因此不适用;
  • p0593r6(即C++23中关于隐式对象创建的提案):隐式对象创建允许在满足对齐和大小要求的内存上无需显式构造就能创建对象,但它解决不了严格别名问题——依旧不能用T*去别名std::byte数组。

问题2:std::vector是怎么正确实现的?

std::vector的实现从根源上避免了这个问题:它不会用std::byte数组作为底层存储,而是直接分配未初始化的内存,而非创建std::byte(或char)数组。

具体来说,std::vector会通过分配器(比如默认的std::allocator<T>)调用allocate(n)函数,返回的是指向足够容纳nT对象的未初始化内存的指针(C++17及以后是allocator_traits<Alloc>::pointer,本质等价于T*)。这块内存上没有任何存活对象,不存在“原数组对象和新对象共存”的冲突。

当需要构造对象时,vector会用placement new在这块内存上构造T对象:new(ptr) T(args),之后直接通过原本的T*指针访问对象完全符合标准——因为指针本身就是T*类型,内存上也没有其他存活对象,严格别名规则不适用。

部分旧实现可能用char*分配内存,但它们会确保构造T对象后,不再以char*类型访问这块内存,而是直接转换为T*使用。这种写法在严格标准下虽有瑕疵,但编译器会对标准库实现做特殊兼容处理,实际运行无问题。而现代vector实现均采用直接分配未初始化内存的方式,彻底规避了严格别名问题。


内容的提问来源于stack exchange,提问作者Ashcoll Ash

火山引擎 最新活动