2

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 :)

Barry
  • 286,269
  • 29
  • 621
  • 977
SomeProgrammer
  • 1,134
  • 1
  • 6
  • 12
  • "*the new c++20 standard it says:*" Where does it say that? The closest I can find is [class.temporary]/6.10, which [doesn't say that](https://timsong-cpp.github.io/cppwp/n4861/class.temporary#6.10). – Nicol Bolas Feb 12 '21 at 17:08
  • I found it in cppreference, on a page called “reference initialisation”. This bit works, (under GCC), it’s the placement new and std::launder code which is problematic. – SomeProgrammer Feb 12 '21 at 17:12
  • 1
    If you're going to quote some text, you should make it clear what you're quoting. I assumed you were quoting the C++ standard, not a reference doc. Especially since *that's what you said you quoted*. Cppreference is *not* "the C++20 standard". – Nicol Bolas Feb 12 '21 at 17:16
  • 3
    I don't think there is a connection between `std::launder` and the segmentation fault you're seeing. `std::launder`, to my understanding, helps preventing the compiler from making some assumptions about pointers, mainly about constness of values and dynamic types. most developers shouldn't even know about it, it's super specific. this looks like a regular dangling pointer/uninitialized pointer. – David Haim Feb 12 '21 at 17:18
  • You can't just placement-new over a live class.... – Mooing Duck Feb 12 '21 at 17:30
  • @Mooing Duck Why is that? – SomeProgrammer Feb 12 '21 at 17:31
  • @Cyrus: Because the destructor wsn't called. Placement new over a live class with a constructor/destructor _has_ to be illegal... right? – Mooing Duck Feb 12 '21 at 17:32
  • 1
    @MooingDuck: It is actually legal. Of course, if the thing was a lock, or the compiler thought it had static lifetime and later destructs a not-the-thing, or, or, or... then you're gonna get bugs or UB. But generally it is permitted. – GManNickG Feb 12 '21 at 17:33
  • @GManNickG - IDNKT. But I don't get how "actually legal" matches up with "you're gonna get .. UB" - usually that doesn't happen. So ... can you please point me to where in the standard (or cppreference.com, I'm not picky!) it says that it's legal? Because ... seems unexpected, huh? – davidbak Feb 12 '21 at 17:39
  • 2
    @davidbak: My "gonna get UB" has conditionals before it for a reason. :) I'll reference https://stackoverflow.com/a/41385385/87234 which contains the right emphasized quote. What I mean is this is C++: just because you can doesn't mean it's safe to do so. You are free to reuse the storage for an automatically (stack) allocated object for an entirely new thing - but don't be surprised when the compiler tries to destruct the original object and now you get UB. But you are free to e.g. `new vector` and then replace it entirely with something else (leaking memory, of course, but that's allowed). – GManNickG Feb 12 '21 at 17:45
  • 2
    @GManNickG - Huh, the way I read it is that it is UB _if_ you don't call a non-trivial destructor "and it has [observable] side effects". But I think that means it _is not_ legal - unless the conditions are met! - which I believe is something that comes up often in the standard. So I'd go with "it is actually not legal _unless_ ...". Do you see it the same? – davidbak Feb 12 '21 at 17:53

2 Answers2

4

[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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • So there is no way to reassign a value to my polymorphic object? – SomeProgrammer Feb 12 '21 at 17:22
  • 5
    @Cyrus: It has nothing to do with "reassignment" and *everything* to do with "reassignment *to a temporary*." Stop relying on esoteric rules of temporary lifetime extension, and all your problems go away. Learning to write C++ code that relies on such rules only means that you're writing C++ code that requires a reader to have detailed knowledge of standard arcana to see if it works. You shouldn't do that; write *clear* code first. – Nicol Bolas Feb 12 '21 at 17:23
0

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.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226