std::launder 是c++17引入的用于在对象原地重建后使旧指针合法访问新对象的语义提示函数,仅在placement new销毁并重建同一地址对象且持有原指针时必须使用,否则导致未定义行为和错误优化。

std::launder 是什么,什么时候必须用它
std::launder 不是“让指针变合法”的万能补丁,而是告诉编译器:“这个指针指向的对象,虽然内存地址没变,但它的生命周期已经重新开始,别用旧的假设优化它”。它只在对象被原地销毁又重建(placement new + 析构)且你持有旧指针时才需要——比如 std::variant 内部、自定义联合体、或手动管理对象生命周期的池分配器里。
常见错误现象:程序在 -O2 下行为异常,比如读到垃圾值、断言失败、甚至静默跳过某段逻辑。这是因为编译器看到你用旧指针访问新对象,误判为未定义行为,直接优化掉了读取或重排序了指令。
必须用 std::launder 的典型场景:
- 用 placement new 在已构造对象的存储上创建新对象后,仍持有原始指针
- 调用旧对象的析构函数后,立即在同一地址构造新对象,并想通过原指针访问
- 标准库容器(如
std::vector)内部做内存重用时,某些实现依赖它保证指针有效性
不加 std::launder 会出什么问题
最典型的例子是编译器把两次访问合并或消除。比如:
struct X { int a; };
alignas(X) char buf[sizeof(X)];
X* p = new(buf) X{42};
p->a = 100;
p->~X();
new(buf) X{200}; // 新对象诞生
// 此时 p 指向的是新对象的地址,但类型系统里它仍是“旧 X 的指针”
int x = p->a; // ❌ 未定义行为:p 不再指向活跃对象
Clang 和 GCC 在优化级别 ≥ -O1 下很可能把这行 p->a 替换为 100(即复用之前存的值),而不是去内存里读 200。这不是 bug,是标准允许的优化——因为 p 没有被重新绑定到新对象。
关键点:
-
std::launder(p)返回一个“新指针”,它被标准认定为可安全指向新对象 - 不加它,哪怕地址相同、类型相同,访问也是未定义行为
- 即使你用
reinterpret_cast或memcpy绕开,也不能替代std::launder的语义作用
std::launder 的正确用法和限制
它只接受指向对象的指针(不能是 void* 或函数指针),返回同类型的指针;它不改变地址、不分配内存、不调用构造函数,纯属“语义提示”。
实操建议:
- 必须在新对象构造完成之后、首次访问之前调用:
X* q = std::launder(p); - 不能对未构造对象的地址调用:
std::launder(buf)是错的,必须传指针,且该指针原本指向某个对象(哪怕已被析构) - 不能用于数组首元素以外的地址:比如
&arr[1]不能直接launder,除非你能证明它对应某个独立对象的起始地址 - C++17 起可用,头文件是
<memory></memory>;C++20 起支持对const指针使用
示例修正:
X* p = new(buf) X{42};
p->~X();
X* q = new(buf) X{200}; // 构造新对象
X* r = std::launder(p); // ✅ 告诉编译器:p 现在可以指向新对象了
assert(r->a == 200); // 现在安全且可靠
容易忽略的边界情况
很多人以为“只要地址一样就能 launder”,但标准要求更严:目标地址必须是某个对象的起始地址,且该对象类型与指针类型满足严格别名规则。比如:
以下都是无效用法:
- 对
char*缓冲区首地址直接std::launder,而不先用 placement new 构造对象 - 对联合体中非活跃成员的地址调用(即使它和活跃成员共享内存)
- 跨继承边界 launder:基类子对象地址不能直接
launder成派生类指针,除非你确切知道布局且满足标准规定 - 在对象生命周期外调用:比如对象还没构造完就
launder,或已被destroy_at后又没重建就调用
真正安全的做法永远是:先确保新对象已完全构造,再用 std::launder 包装一个曾指向该内存中旧对象的指针——不是任意指针,也不是临时算出来的地址。这点稍不注意,就从“修复 UB”变成“制造新 UB”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











