UB是否保留一个指向已销毁对象的指针,然后使用它访问重新创建的对象,可能指向第一个子对象



考虑

struct full
{
struct basic
{
int a = 1;
} base;
int b = 2;
};
void example()
{
alignas(full) std::byte storage[/* plenty of storage */];
full * pf = new (storage) full;
basic * pb = &pf->base;
new (storage) basic; // supposedly ends ​lifetime of *pf (right?)
// if doesn't, suppose we did pf->~full(); before this line
pb->a; // is this legal?
new (storage) full; // supposedly ends ​lifetime of *pb (right?)
// if doesn't, suppose we did pb->~basic(); before this line
pb->a; // is this still legal?
pf->b; // is this legal?
pf->base.a; // is this legal?
}

我想知道以上任何一项是否合法,包括了解在每个步骤之前是否需要析构函数调用。

代码的编写方式具有未定义的行为,因为一旦对象被破坏(即在new (storage) basic;点),pfpb都会停止指向对象。实际上,编译器可以自由推测通过new (storage) basic;表达式中的这些指针可以访问的值。例如,读取这些指针可以产生编译器根据以前通过这些指针写入的值,但不一定通过指向新构建对象的指针。

该标准具有std::launder功能来缓解这种情况。该函数有效地充当了基于指针和它所指向的对象的编译器推测的屏障;擦除";编译器可能已经掌握的关于指向对象的任何知识,并返回一个指针,该指针似乎是重新获得的。更正后的代码如下所示:

void example()
{
alignas(full) std::byte storage[/* plenty of storage */];
full * pf = new (storage) full;
basic * pb = &pf->base;
new (storage) basic;
pb = std::launder(pb); // prevent speculation about the object pb points to
pb->a; // ok now
new (storage) full;
pf = std::launder(pf); // prevent speculation about pf and pb
pb = std::launder(pb);
// These are ok now
pb->a;
pf->b;
pf->base.a;
}

21.6.4[ptr.slaugh]状态

  1. 注意:如果在现有对象占用的存储中创建新对象对象,则可以使用指向原始对象的指针引用新对象,除非该类型包含常量或引用成员;在后一种情况下,此函数可用于获得指向新对象的可用指针

。。。因此,在结构包含常量或引用的情况下,通常使用launder-it是一种很好的做法。但我相信在以下例子中:

alignas(full) std::byte storage[/* plenty of storage */];
full * pf = new (storage) full;
basic * pb = &pf->base;
new (storage) basic; // supposedly ends ​lifetime of *pf (right?)
pb->a; // is this legal?

pb->a;是合法的,即如果指向的结构不包含const成员,也不包含引用,则它是NOT和UB。因此,在这里使用launder是一种很好的做法。但在满足上述条件的情况下,这并不是强制性的。

最新更新