2

In the following code (build on gcc 9.2 with -std=c++14 -Wall -fno-elide-constructors):


struct Noisy {
    Noisy() { std::cout << "Default construct [" << (void*)this << "]\n"; }
    Noisy(const Noisy&) { std::cout << "Copy construct [" << (void*)this << "]\n"; }
    Noisy(Noisy&&) { std::cout << "Move construct [" << (void*)this << "]\n"; }
    Noisy& operator=(const Noisy&) { std::cout << "Copy assignment" << std::endl; return *this; }
    Noisy& operator=(Noisy&&) { std::cout << "Move assignment" << std::endl; return *this; }
    ~Noisy() { std::cout << "Destructor [" << (void*)this << "]\n"; }
};

Noisy f() {
    Noisy x;
    return x;
}

Noisy g(Noisy y) {
    return y;
}
int main(void) {
    Noisy a;
    std::cout << "--- f() ---\n";
    Noisy b = f();
    std::cout << "b [" << (void*)&b << "]\n";
    std::cout << "--- g(a) ---\n";
    Noisy c = g(a);
    std::cout << "c [" << (void*)&c << "]\n";
    std::cout << "---\n";
    return 0;
}

Which produces this outcome:

Default construct [0x7ffc4445737a]
--- f() ---
Default construct [0x7ffc4445735f]
Move construct [0x7ffc4445737c]
Destructor [0x7ffc4445735f]
Move construct [0x7ffc4445737b]
Destructor [0x7ffc4445737c]
b [0x7ffc4445737b]
--- g(a) ---
Copy construct [0x7ffc4445737e]
Move construct [0x7ffc4445737f]
Move construct [0x7ffc4445737d]
Destructor [0x7ffc4445737f]
Destructor [0x7ffc4445737e]
c [0x7ffc4445737d]
---
Destructor [0x7ffc4445737d]
Destructor [0x7ffc4445737b]
Destructor [0x7ffc4445737a]

Why does the copy of the local Noisy object [0x7ffc4445735f] in f() gets destructed right after it has been moved into f's return address (and before construction of b starts); while the same does not seem to happen for g()? I.e. in the latter case (when g() executes), the local copy of the function argument Noisy y, [0x7ffc4445737e] gets destroyed only after c is ready to be constructed. Shouldn't it have been destroyed right after it was moved into g's return address, same as it happened for f()?

M.M
  • 130,300
  • 18
  • 171
  • 314
  • I edited the code to use different names for each variable, this will avoid confusion when the answer talks about each variable – M.M Mar 17 '20 at 06:48

2 Answers2

2

These are the variables for the addresses in the output:

0x7ffc4445737a  a
0x7ffc4445735f  x
0x7ffc4445737c  return value of f() 
0x7ffc4445737b  b
0x7ffc4445737e  y
0x7ffc4445737f  return value of g()
0x7ffc4445737d  c

I interpret the question as: you are highlighting the following two points:

  • x is destroyed before b is constructed
  • y is destroyed after c is constructed

and ask why both cases didn't behave the same.


The answer is: in C++14 the standard specified in [expr.call]/4 that y should be destroyed when the function returns. However it was not clearly specified at exactly which stage of the function return this meant. A CWG issue was raised.

As of C++17 the specification is now that it's implementation-defined whether y is destroyed at the same time as local variables of the function, or at the end of the full-expression containing the function call. It turned out that the two cases could not be reconciled because it would be a breaking ABI change (think what happens if the destructor of y throws an exception); and also the Itanium C++ ABI specifies destruction at the end of the full-expression.

We can't definitively say that g++ -std=c++14 doesn't conform to C++14 due to the ambiguity of the C++14 wording, however in any case it's not going to be changed now due to the ABI issue.

For an explanation with links to the Standard and the CWG report, see this question: Sequencing of function parameter destruction and also Late destruction of function parameters

M.M
  • 130,300
  • 18
  • 171
  • 314
  • Thank you, this gives me peace of mind :) Looking under the hood while taking the standard literally can sometimes be distressing, e.g. https://stackoverflow.com/questions/33344259/does-rvo-work-on-object-members – user12655242 Mar 17 '20 at 23:50
1

The difference is quite clear if you look at the generated assembly (for example on the compiler explorer).

Here you can see that for the call to g the argument object is actually created and destructed in the main function.

So for the g function the output order is

  1. Copy-construction of the argument y from a
  2. Call the function g, passing y
  3. Inside the function g, y is moved into the temporary return object
  4. The function g returns
  5. Back in main the temporary return object is moved into c
  6. The temporary return object is destructed
  7. The argument object y is destructed

For the function f the local object x is constructed and destructed inside the scope of f:

  1. f is called
  2. x is default-constructed
  3. The temporary return object is move-constructed from x
  4. x is destructed
  5. The function f returns
  6. The temporary return object is moved into b
  7. The temporary return object is destructed
Some programmer dude
  • 363,249
  • 31
  • 351
  • 550
  • Yes, thank you, the generated code does show gcc's interpretation. But what I was trying to point out is the apparent inconsistency in which, using your steps for 'g', g7 does not happen in between g3 and g4. Given that for f, step 'x is destructed', does happen between f3 and f5... – user12655242 Mar 17 '20 at 23:49