同一内存地址存在两个同类型对象时std::launder的指针指向问题咨询
我最近在研究std::launder的时候,碰到了一个标准里似乎没明确覆盖的场景,想来和大家探讨一下。
先给大家回顾一下cppreference对std::launder(T *p)的定义:
形式上,假设指针
p代表内存中某个字节的地址A:
- 有一个对象x位于地址A处
- x正处于其生命周期内
- x的类型与T相同(忽略所有层级的cv限定符)
- 通过返回结果可访问的每个字节,都能通过p访问到
...
那么std::launder(p)会返回一个指向对象x的T*类型值。否则行为未定义。
我对这段定义的理解是:如果p指向的内存位置上存在一个T类型对象,那么std::launder(p)就会返回指向该T对象的指针。之前看到的例子大多是讲std::launder用来处理“一个对象生命周期结束,另一个对象在同一块内存开始生命周期”的场景,用来告诉编译器这个变化。但按我的理解,它的用法其实不止于此。
比如下面这个例子:
struct S { int i; S(int i, int j) : i(i), j(j) {} private: int j; }; int main() { S s(10, 20); int *pi = reinterpret_cast<int*>(&s); int *pi_good = std::launder(pi); }
这里pi是一个int*,它指向的内存位置上确实存在一个int对象,但指针不只是指向内存地址,更是指向对象。因为S和int不是指针可互转的类型,所以pi实际上仍然指向的是S对象,直接解引用pi是未定义行为。而std::launder在这里就派上用场了,它能给我们一个真正指向那个int对象的指针。
这就扩展了std::launder的适用场景——不止是对象替换的情况,甚至在同一内存地址同时存在两个不同对象的时候,我们也能用它在指向不同对象的指针之间切换(反过来也一样)。(这也意味着std::launder(reinterpret_cast<T*>(std::rand()))在某些情况下可能是合法的。)
现在我遇到的核心问题是:如果同一内存地址上同时存在两个同类型的对象,对指向该地址的指针调用std::launder后,返回的指针会指向哪一个对象?
比如下面这个例子:
#include <new> struct Holder { // 用unsigned char提供存储 unsigned char storage[4]; // 让它成为非平凡类型 ~Holder() {}; template <typename T> void hold() { ::new(static_cast<void *>(storage)) T; } }; int main() { Holder h; h.hold<Holder>(); Holder *ph = &h; [[maybe_unused]] Holder *ph_laundered = std::launder(ph); }
这里Holder对象h和它内部存储里构造的另一个Holder对象,同时存在于同一个内存地址上,而且类型完全相同。那么调用std::launder(ph)后,返回的指针是指向原来的h,还是指向新构造的那个Holder对象呢?
*补充一下之前例子里的细节:定义里最后一个要求是,S*不能访问任何int*无法访问的字节。我们可以把S改成如下定义,既保持S和int不可指针互转,又满足这个要求:
class S { private: int i; };
内容来源于stack exchange

