c++compiler-optimizationstandardslifetimeimplicit-lifetime

Why are [[no_unique_address]] members not transparently replaceable?


In the classic talk An (In-)Complete Guide to C++ Object Lifetimes by Jonathan Müller, there is a useful guideline as follows:

Q: When do I need to use std::launder?

A: When you want to re-use the storage of:

  • const heap objects,
  • base classes, or
  • [[no_unique_address]] members.

The first bullet is obvious, but the other two bullets are a bit hard to grasp.

For example, consider the following code:

struct Empty {};

struct A {
    [[no_unique_address]] Empty e = {};
    char                        c = {};
};

int main() {
    auto a  = A{};
    auto p1 = &a.e;
    auto p2 = new (&a.c) char;
    (void)p2->e; // OK.
    (void)*p1;   // UB due to bullet 3.
}

The UB is originally due to this C++ standard rule :

An object o1 is transparently replaceable by an object o2 if ... and neither o1 nor o2 is a potentially-overlapping subobject.

What I cannot understand are:

  • Why does the C++ standard impose such a special restriction on potentially-overlapping subobjects?
  • What bad consequences would follow without this restriction?

The rule covers both bullet 2 (in case of Empty Base Optimization) and bullet 3.


Solution

  • According to cppref:

    If the [[no_unique_address]] member is not empty, any tail padding in it may be also reused to store other data members.

    The following code can demonstrate why std::launder is necessary in such a scenario:

    struct alignas(std::uint16_t) A { std::uint8_t x; };
    
    struct B {
        [[no_unique_address]] A a = {};
        std::uint8_t            y = {};
    };
    
    static_assert(2 == sizeof(A)); // likely true
    static_assert(2 == sizeof(B)); // likely true
    
    int main() {
        auto b = B{.a{.x = 7}, .y = 8};
        assert(8 == b.y);           // true
        assert(&b.a.x + 1 == &b.y); // likely true
        b.a.~A();
        new (&b.a) A{.x = 9}; 
    
        // Now the value of y is indeterminate.
        (void)std::launder(&b)->y; // Well-defined. Read the latest value of y.
        (void)b.y; // UB. The compiler may assume the value of y is still 8!
    }