Search code examples
c++language-lawyerstdlaunder

Do I need `std::launder` when working with unions?


Consider the following code:

#include <iostream>
#include <string>

union U
{
    std::string s;

    U()
    {
        new(&s) std::string;
    }

    void Set(std::string new_s)
    {
        s.~basic_string();
        new(&s) std::string(std::move(new_s));
    }

    ~U()
    {
        s.~basic_string();
    }
};

int main()
{
    U u;
    u.Set("foo");
    std::cout << u.s << '\n'; // Do I need `std::launder` here?
}

I know that if I used a byte array instead of a union, I'd have to std::launder(&s) to access the string. But do I need it here?

The common sense answer seems to be "no", but then where does the standard bless unions to not need std::launder?


Solution

  • The member access expression u.s is simply defined to result in a lvalue to the s member subobject of the union. (That is true even if u.s is not in its lifetime, i.e. not active.) See [expr.ref]/6.2.

    So there can't be any need to launder. You already have a glvalue to the object you want.

    Even if you start from a pointer to the union object instead and attempt to use a pointer cast approach, more similar to the byte array approach, then it will still work without std::launder, e.g.:

    std::cout << *reinterpret_cast<std::string*>(&u) << '\n';
    

    That is because a union object is pointer-interconvertible with any of its member subobjects per [basic.compound]/4.2, meaning that the reinterpret_cast will produce a pointer to the member subobject immediately per [expr.static.cast]/14.

    std::launder is required if the result of a cast still points to the original object due to lack of pointer-interconvertibility between the source and the target object.


    And also, per [basic.object]/2 together with [basic.life]/8 the objects you create with new will become member subobjects of the union and s in u will designate the most recent such subobject, because they satisfy all of the requirements mentioned in those paragraphs (in particular exact overlap with s and exact type match with s).

    Therefore s.u will also not accidentally refer to the previous std::string subobject that has been replaced with a later new.