Home > Software engineering >  C 20 - Is it UB to keep a pointer to a destroyed object and then use it to access re-created object
C 20 - Is it UB to keep a pointer to a destroyed object and then use it to access re-created object

Time:10-15

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.

CodePudding user response:

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;
}

CodePudding user response:

21.6.4 [ptr.launder] states

  1. Note: If a new object is created in storage occupied by an existing object of the same type, a pointer to the original object can be used to refer to the new object unless the type contains const or reference members; in the latter cases, this function can be used to obtain a usable pointer to the new object.

...so generally it's a good practice to use launder it in case the struct contains consts or references. But I believe that in the following 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?)
    pb->a; // is this legal?

pb->a; is legal, i.e. it's NOT and UB provided the struct pointed to does not contain const members, neither does it contain references. So using launder here is a good practice, true. But it's not mandatory given the aforementioned conditions are met.

  • Related