Why does using placement new on reference types give me a segmentation fault, even with std::launder?

Viewed 143

In the new C++20 standard, cpprefrence says:

a temporary bound to a reference in a reference element of an aggregate initialized using direct-initialization syntax (parentheses) as opposed to list-initialization syntax (braces) exists until the end of the full expression containing the initializer. Example:

struct A {
   int&& r;
};
A a1{7}; // OK, lifetime is extended
A a2(7); // well-formed, but dangling reference

NOTE: I am using GCC as this is the only compiler which supports this feature. Having read this, and knowing that references are polymorphic, I decided to create a simple class:

template <typename Base>
struct Polymorphic {
    Base&& obj;
};

So that the following code works:

struct Base {
    inline virtual void print() const {

        std::cout << "Base !\n";
    }
};

struct Derived: public Base {
    inline virtual void print() const {

        std::cout << "Derived !" << x << "\n";
    }

    Derived() : x(5) {}
    int x;
};

int main() {
    Polymorphic<Base> base{Base()};
    base.obj.print(); // Base !
    Polymorphic<Base> base2{Derived()};
    base2.obj.print(); // Derived 5!
}

The problem I encountered is when changing the value of my polymorphic object (not the value of obj, but the value of a polymorphic object itself). Since I can't reassign to r-value references, I tried to do the following using placement new:

#include <new>
int main() {
    Polymorphic<Base> base{Base()};
    new (&base) Polymorphic<Base>{Derived()};
    std::launder(&base)-> obj.print(); // Segmentation fault... Why is that?
    
    return 0;
}

I believe I have a segmentation fault because Polymorphic<Base> has a reference sub object. However, I am using std::launder - isn't that supposed to make it work? Or is this a bug in GCC? If std::launder does not work, how do I tell the compiler not to cache the references?

On a side note, please do not tell me "your code is stupid", "use unique pointers instead"... I know how normal polymorphism works; I asked this question to deepen my understanding of placement new and std::launder :)

2 Answers

[class.temporary]/6.12 states:

A temporary bound to a reference in a new-initializer persists until the completion of the full-expression containing the new-initializer.

It is not picky about how the reference is initialized; this applies to all ways such references get initialized. Indeed, there's even an example:

struct S { int mi; const std::pair<int,int>& mp; };
S a { 1, {2,3} };
S* p = new S{ 1, {2,3} };       // creates dangling reference

Placement-new is a new-initializer. So it applies. Just like the above, base contains a dangling reference. It doesn't matter how you access it after that point; the object it references has been destroyed, so you get UB.

If the lifetime of a temporary is not obvious by the reader of some code, you should not be using a temporary. Just give it a name, and all your problems go away.

Look at the bullet point immediately above the one you quoted in the link you provided:

a temporary bound to a reference in the initializer used in a new-expression exists until the end of the full expression containing that new-expression, not as long as the initialized object. If the initialized object outlives the full expression, its reference member becomes a dangling reference.

In your crashing example, you are using a new expression, so the lifetime is not extended.

Related