Consider
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?
}
I would like to know if any of the above is legal or not, including understanding whether the destructor call is necessary before each step.
The way it is written, your code has undefined behavior because both pf
and pb
stop pointing to an object as soon as it is destroyed (i.e. at the point of new (storage) basic;
). In practical terms, the compiler is free to speculate the values that are accessible through these pointers across the new (storage) basic;
expression. For example, reading through these pointers could produce values that the compiler speculated based on the previous writes through these pointers, but not necessarily through pointers to the newly constructed object.
The standard has std::launder
function to mitigate this. The function effectively acts as a barrier for compiler speculations based on the pointer and the object it points to. Basically, it "erases" any knowledge the compiler might have had about the pointed object, and returns a pointer that was as if obtained anew. The corrected code would look like this:
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;
}