通过指针类型转换访问生命周期内对象是否属于未定义行为?
先看这段代码:
struct WString_header { WString_header(std::wstring_view str) : m_size(str.size()) { // 只要分配的字节存储能容纳数组,这不应是未定义行为? wchar_t *chars = new (this + 1) wchar_t[str.size()]; std::copy(str.begin(), str.end(), chars); } size_t size() const { return m_size; } const wchar_t *chars() const { // 是否为未定义行为? return reinterpret_cast<const wchar_t *>(this + 1); } protected: size_t m_size; };
核心疑问拆解
构造函数中的placement new是否合法?
只要this + 1指向的内存区域大小足够容纳str.size()个wchar_t对象,这个placement new本身是合法的——它确实在该内存位置启动了wchar_t数组的生命周期。chars()中的reinterpret_cast解引用是否属于未定义行为?
这才是问题的关键:你之前的认知有偏差,即使内存位置已经有了合法对象,用reinterpret_cast得到的指针直接解引用也可能是未定义行为。
C++标准中,指针的"指向关系"是严格的:this + 1是指向WString_header类型对象末尾之后的指针,你通过reinterpret_cast把它转成const wchar_t*,但这个转换后的指针并没有被标准认可为指向那个已经构造好的wchar_t数组。编译器可能会基于指针的原始类型(WString_header*的偏移)做优化,比如认为该地址不会被当作wchar_t类型访问,从而导致错误的代码生成。
而std::launder的作用就是告诉编译器:"这个指针指向的内存位置已经有了一个新对象,你要忽略指针的原始类型关联,把它当作指向这个新对象的合法指针"。所以你的chars()函数应该改成:
const wchar_t *chars() const { return std::launder(reinterpret_cast<const wchar_t *>(this + 1)); }
这样才能确保解引用是定义行为。
对std::launder与std::start_lifetime_as的理解纠正
你引用的P2590R2文档描述是准确的:
另一方面,std::launder从不创建新对象,仅能用于获取指向已存在于指定内存位置、且已通过其他方式启动生命周期的对象的指针。
std::start_lifetime_as则是用来启动内存位置的对象生命周期(不需要placement new),而std::launder是用来修正指针指向关系——当内存里已经有对象,但指针的类型和对象类型不匹配时,必须用launder来让指针合法指向该对象。
reinterpret_cast的意义
reinterpret_cast转换指针并非只能用于运算和比较,它的合法场景包括:
- 在不同指针类型之间转换,之后通过
std::launder修正后访问对象 - 转换为整数类型保存地址(比如序列化场景)
- 在符合别名规则的类型之间转换(比如
char*和任意对象指针之间)
你的代码之前能运行,确实是侥幸避开了编译器优化——比如编译器没有对指针类型做严格的别名分析或优化。一旦编译器开启更高等级的优化,或者代码结构更复杂,就可能出现未定义行为导致的崩溃或错误结果。
内容的提问来源于stack exchange,提问作者user19232978

