3

While toying with RVO I encountered the following issue and I can't get my head around it.

#include <iostream>
struct A {
    A* p = this;
};

A func()
{
    return A();
}

int main()
{
    A a = A();
    std::cout << "address of a: " << &a << " - a.p = " << a.p << std::endl;

    A b = func();          
    std::cout << "address of b: " << &b << " - b.p = " << b.p << std::endl;
}

Result:

address of a: 0x7ffe5b11b7a0 - a.p = 0x7ffe5b11b7a0

address of b: 0x7ffe5b11b790 - b.p = 0x7ffe5b11b770

All fine with a. However note that b.p doesn't actually point to b as if it was pointing to a different object.

I'm using gcc 8.3.0 which supports C++17. My understanding is that from C++17 onwards, RVO is mandatory and initialisation from a prvalue (i.e. A b = func();) should also trigger mandatory elision. Therefore, in theory, only one constructor should be called to initialise b so p should not be able to point to anything else than b. Could someone explain me what is happening here?

Note: initialising p in a default constructor instead of using default member initialisation doesn't change the behaviour.

I toyed with this a bit more and realised that defining a default constructor AND any one of the following special functions makes p point to b as expected:

  • destructor
  • move constructor
  • move assignment operator
  • copy constructor

Instead of helping me understand the problem, this confused me even more...

As a side note, I have witnessed the exact same behaviour with gcc 11.0.0 and clang 11.0.0 (using Wandbox) so this is unlikely a bug.

  • To get more info and data points, try adding a default constructor that dumps `this` to `std::cout`, and see what that gives you. – Sam Varshavchik Jun 16 '20 at 12:19
  • Indeed, I've done that as a test and it was always equal to p. – Christophe Messaouik Jun 16 '20 at 12:22
  • 1
    http://eel.is/c++draft/class.temporary#3.sentence-1 – cpplearner Jun 16 '20 at 12:25
  • 1
    Interestingly, if the copy constructor is defined as deleted, then it behaves as expected. Edit: Rule linked by cpplearner explains this. – eerorika Jun 16 '20 at 12:25
  • @eeroika Here is [live demo](https://wandbox.org/permlink/2OMCdIgmIfnWldJJ). When the deleted definitions are removed, addresses are different. It's totally weird. – Daniel Langr Jun 16 '20 at 12:28
  • Small trivial object, same answer here applies https://stackoverflow.com/questions/48879226/does-the-behavior-of-guaranteed-copy-elision-depend-on-existence-of-user-defined (probably a duplicate). – StoryTeller - Unslander Monica Jun 16 '20 at 12:30
  • @StoryTeller-UnslanderMonica Nice finding, don't remember my own answers ;-). Just a note that the rule was likely motivated by passing objects in registers, while here we explicitly require them to be in memory to be addressed. – Daniel Langr Jun 16 '20 at 12:38
  • Thanks to cpplearner and StoryTeller. It looks like a duplicate indeed. So if I understand this properly, this is a case where a temporary can be created by the compiler and p points to that temporary. – Christophe Messaouik Jun 16 '20 at 12:41
  • @StoryTeller-UnslanderMonica A tangential topic: do you believe cppreference's [page on copy elision](https://en.cppreference.com/w/cpp/language/copy_elision) is inaccurate w.r.t. this temporary object corner case? Particularly the section _Mandatory elision of copy/move operations_, which seems to cover return value elision solely from the perspective of _"unmaterialized value passing"_ whereas, as your linked answer covers, a temporary _may_ actually be materialized in some circumstances. – dfrib Jun 16 '20 at 13:03
  • 1
    @dfri - Not so much inaccurate as incomplete. It's correct in asserting that no value is materialized (as opposed to elided). But it doesn't cover when materialization should/may occur. – StoryTeller - Unslander Monica Jun 16 '20 at 13:07

0 Answers0