3

Consider the following snippet:

#include <iostream>

using namespace std;

class Temp {
    public:
    Temp() { cout << "Temp()" << endl;}
    ~Temp() { cout << "~Temp()" << endl;}
};

Temp GetTemp() {
     cout << "GetTemp" << endl;
    return Temp();
}

Temp TakeTemp(Temp temp) {
    cout << "TakeTemp" << endl;
    return temp;
}


int main()
{
    TakeTemp(GetTemp());

    return 0;
}

When I ran TakeTemp(GetTemp());, the output looks like

GetTemp                                                                                                                                                        
Temp()                                                                                                                                                         
TakeTemp                                                                                                                                                       
~Temp()                                                                                                                                                        
~Temp()     

Note that ~Temp() is called twice here (but only 1 temp obj is constructed). This seems odd since 1) the temp variable returned by GetTemp() is supposed to have its lifetime extended to the full expression, and 2) since we return temp directly in TakeTemp, return value optmization will reuse the same object.

Can anyone explain why there are multiple dstor calls here?

(Note that if we place more layers of TakeTemp() around, the number of dstor calls grows proportionally.)

OneZero
  • 11,556
  • 15
  • 55
  • 92
  • Added full program there. – OneZero Jan 31 '19 at 08:33
  • 1
    Also there is no lifetime extension here (in the parts of the code posted so far). The lifetime of a temporary is until the end of the full-expression. – M.M Jan 31 '19 at 08:33
  • Isn't the full expression `TakeTemp(GetTemp());`? In that case the same temp variable will live all the way thru. – OneZero Jan 31 '19 at 08:34
  • 1
    Yes that is the full expression. The problem is you use the word "extend" to describe what its natural lifetime was – M.M Jan 31 '19 at 08:40
  • The implicit copy constructor is being called. Add `Temp(Temp const&) { cout << "Temp(Temp const&)" << endl;}` – Eljay Jan 31 '19 at 12:38

2 Answers2

5

Your function TakeTemp takes its argument by value, and returns the argument by value.

You're making a copy there, hence there are now two Temp objects to delete.

The two objects you are seeing destructed are the return values of the two functions called here:

TakeTemp(GetTemp());
         ^ returns a Temp
^ returns a Temp
rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • Why is cstor only called once then? – OneZero Jan 31 '19 at 08:34
  • 1
    @OneZero Because you are not logging copy / move constructors. – user7860670 Jan 31 '19 at 08:35
  • @OneZero: because you only trace the default constructor and the copy uses the copy constructor. – Serge Ballesta Jan 31 '19 at 08:35
  • Be advised that putting anything (e.g. logging) in the copy constructor will not be guaranteed to be visible at all, and since C++17 actually will never show up in this case. – rubenvb Jan 31 '19 at 08:36
  • 1
    Go add a copy constructor of the form `Temp(const Temp& other) { cout << "Temp(other)" << endl;}` too see it in your program output. Without that, the compiler generates a default copy constructor for you. – selbie Jan 31 '19 at 08:36
  • 2
    @rubenvb It will show up at lest once because copying of function parameter `temp` can not be elided. – user7860670 Jan 31 '19 at 08:38
1

Using C++17 terminology, the two objects are:

  1. The function parameter Temp temp;
  2. The return value of TakeTemp.

The function call GetTemp() is a prvalue. Since it is the argument to a function call, its result object is the matching parameter Temp temp. The temporary materialization conversion is applied at the point of construction of Temp temp.

Note that there is no temporary created inside the GetTemp() function. The statement return Temp(); does not mean to create an object; it's giving the arguments that will be used to ultimately create an object later. There is no object created until a prvalue is materialized.

Then, the exection of return temp; creates the second object. This differs from return Temp(); because temp is an lvalue, not a prvalue. The return value object of the function call to TakeTemp is created using temp as the initializer. This is not a copy elision context. If you add a copy constructor to Temp you will see the message of copy construction of that object.

To recap, the order of events is:

  • GetTemp body is entered
  • GetTemp return statement is executed, which initializes the parameter Temp temp
  • GetTemp body exits (destroying any local variables if there were any)
  • TakeTemp body is entered
  • TakeTemp return statement is executed, which initializes the return value object for TakeTemp
  • TakeTemp body exits (execution returns to main)
  • { The parameter Temp temp is destroyed
  • { The return value object of TakeTemp is destroyed

The lifetime of Temp temp is the lifetime of a function parameter; it is destroyed after the function returns.

The lifetime of the return value of TakeTemp is a temporary object lifetime, so it lasts until the end of the full-expression.

Note that there is a quirk with function parameter lifetime: it is implementation-defined whether this is destroyed immediately following the call, or at the end of the full-expression. So the last two steps in my list above could occur in either order. On my installation of g++ 8.2.1, the function parameter is actually the later of the two destructors.

M.M
  • 138,810
  • 21
  • 208
  • 365