3

I'm trying to establish whether it is safe for a C++ function to return an object that has a constructor and a destructor. My understanding of the standard is that it ought to be possible, but my tests with simple examples show that it can be problematic. For example the following program:

#include <iostream>
using namespace std;

struct My
{  My() { cout << "My constructor " << endl; }
  ~My() { cout << "My destructor " << endl; }
};
My function() { My my; cout << "My function" << endl; return my; }

int main()
{ My my = function();
  return 0;
}

gives the output:

My constructor
My function
My destructor
My destructor

when compiled on MSVC++, but when compiled with gcc gives the following output:

My constructor
My function
My destructor

Is this a case of "undefined behavior", or is one of the compilers not behaving in a standard way? If the latter, which ? The gcc output is closer to what I would have expected.

To date, I have been designing my classes on the assumption that for each constructor call there will be at most one destructor call, but this example seems to show that this assumption does not always hold, and can be compiler-dependent. Is there anything in the standard that specifies what should happen here, or is it better to avoid having functions return non-trivial objects ? Apologies if this question is a duplicate.

user1958486
  • 571
  • 3
  • 8
  • Why did you make the function static? – Rapptz Jul 16 '13 at 13:23
  • `My my` is allocated on the stack in your function. When you return, it is popped from the stack, and destructed. You'd need to create it on the heap with keyword new in order to return it from your function and not have it destruct. – crush Jul 16 '13 at 13:25
  • @crush: nope, the function returns a `My` object, so the object should be copied before being destructed. What you are talking about applies to returning *pointers* or *references* to local variables. – Matteo Italia Jul 16 '13 at 13:26
  • @MatteoItalia It would return a copy, not a reference...therefore they aren't the same object. The object declared in the function will always destruct when the function exits. I think I misunderstood what the author was after, though. (thought he was asking why it destructs) – crush Jul 16 '13 at 13:27
  • @crush: sorry, I think I misunderstood what you were meaning. :) – Matteo Italia Jul 16 '13 at 13:34
  • Thanks for these answers. Have modified the code slightly to make the intention clearer. My main question is whether it is normal that the destructor can be called twice for one call of the constructor. – user1958486 Jul 16 '13 at 13:37
  • @user1958486 Since you changed the code, has the output changed? – BoBTFish Jul 16 '13 at 13:51
  • Use `-fno-elide-constructors` to make GCC behave like MSVC (i.e. leave redundant copies in the code instead of optimizing them away) – Jonathan Wakely Jul 16 '13 at 13:57
  • possible duplicate of [What are copy elision and return value optimization?](http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization) – Jonathan Wakely Jul 16 '13 at 14:08

4 Answers4

8

In both cases, the compiler generates a copy constructor for you, that has no output so you won't know if it is called: See this question.

In the first case the compiler generated copy constructor is used, which matches the second destructor call. The line return my; calls the copy constructor, giving it the variable my to be used to construct the return value. This doesn't generate any output.

my is then destroyed. Once the function call has completed, the return value is destroyed at the end of the line { function();.

In the second case, the copy for the return is elided completely (the compiler is allowed to do this as an optimisation). You only ever have one My instance. (Yes, it is allowed to do this even though it changes the observable behaviour of your program!)

These are both ok. Although as a general rule, if you define your own constructor and destructor, you should also define your own copy constructor (and assignment operator, and possibly move constructor and move assignment if you have c++11).

Try adding your own copy constructor and see what you get. Something like

My (const My& otherMy) { cout << "My copy constructor\n"; }
Community
  • 1
  • 1
BoBTFish
  • 19,167
  • 3
  • 49
  • 76
3

The problem is that your class My violates the Rule of Three; if you write a custom destructor then you should also write a custom copy constructor (and copy assignment operator, but that's not relevant here).

With:

struct My
{  My() { cout << "My constructor " << endl; }
   My(const My &) { cout << "My copy constructor " << endl; }
  ~My() { cout << "My destructor " << endl; }
};

the output for MSVC is:

My constructor
My function
My copy constructor
My destructor
My destructor

As you can see, (copy) constructors match with destructors correctly.

The output under gcc is unchanged, because gcc is performing copy elision as allowed (but not required) by the standard.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • thanks, I now understand that the "problem" was the silent copy constructor, and that the compiler dependency was due to a permitted, but not obligatory, optimization – user1958486 Jul 16 '13 at 13:50
3

You are missing two things here: the copy constructor and NRVO.

The behavior seen with MSVC++ is the "normal" behavior; my is created and the rest of the function is run; then, when returning, a copy of your object is created. The local my object is destroyed, and the copy is returned to the caller, which just discards it, resulting in its destruction.

Why does it seem that you are missing a constructor call? Because the compiler automatically generated a copy constructor, which is called but doesn't print anything. If you added your own copy constructor:

My(const My& Right) { cout << "My copy constructor " << endl; }

you'd see

My constructor      <----+
My function              |      this is the local "my" object
My copy constructor   <--|--+
My destructor       <----+  |   this is the return value
My destructor         <-----+

So the point is: it's not that there are more calls to destructors than constructors, it's just that you are not seeing the call to the copy constructor.


In the gcc output, you are also seeing NRVO applied.

NRVO (Named Return Value Optimization) is one of the few cases where the compiler is allowed to perform an optimization that alters the visible behavior of your program. In fact, the compiler is allowed to elide the copy to the temporary return value, and construct the returned object directly, thus eliding temporary copies.

So, no copy is created, and my is actually the same object that is returned.

My constructor  <-- called at the beginning of f
My function    
My destructor   <-- called after f is terminated, since
                    the caller discarded the return value of f
Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
1

To date, I have been designing my classes on the assumption that for each constructor call there will be at most one destructor call [...]

You can still "assume" that since it is true. Each constructor call will go in hand with exactly one destructor call. (Remember that if you handle stuff on the free/heap memory on your own.)

[..] and can be compiler-dependent [...]

In this case it can't. It is optimization depedant. Both, MSVC and GCC behave identically if optimization is applied.

Why don't you see identical behaviour?

1. You don't track everything that happens with your object. Compiler-generated functions bypass your output.

If you want to "follow-up" on the things your compiler does with your objects, you should define all of the special members so you can really track everything and do not get bypassed by any implicit function.

struct My
{  
   My() { cout << "My constructor " << endl; }
   My(My const&) { cout << "My copy-constructor " << endl; }
   My(My &&) { cout << "My move-constructor " << endl; }
   My& operator=(My const&) { cout << "My copy-assignment " << endl; }
   My& operator=(My &&) { cout << "My move-assignment " << endl; }
  ~My() { cout << "My destructor " << endl; }
};

[Note: The move-constructor and move-assignment will not be implicitly present if you have the copy ones but it's still nice to see when the compiler use which of them.]

2. You don't compile with optimization on both MSVC and GCC.

If compiled with MSVC++11 /O2 option the output is:

My constructor
My function
My destructor

If compiled in debug mode / without optimization:

My constructor
My function
My move-constructor
My destructor
My destructor

I can't do a test on gcc to verify if there's an option that enforces all of these steps but -O0 should do the trick I guess.

What's the difference between optimized and non-optimized compilation here?

The case without any copy omittance:

The completely "non-optimized" behaviour in this line My my_in_main = function(); (changed the name to make things clear) would be:

  1. Call function()
  2. In function construct My My my;
  3. Output stuff.
  4. Copy-construct my into the return value instance.
  5. return and destroy my instance.
  6. Copy(or move in my example)-construct the return value instance into my_in_main.
  7. Destroy the return value instance.

As you can see: we have at most two copies (or one copy and one move) here but the compilers may possibly omit them.

To my understanding, the first copy is omited even without optimization turned on (in this case), leaving the process as follows:

  1. Call function()
  2. In function construct My My my; First constructor output!
  3. Output stuff. Function output!
  4. Copy(or move in my example)-construct the return value instance into my_in_main. Move output!
  5. Destroy the return value instance. Destroy output!

The my_in_main is destroy at the end of main giving the last Destroy output!. So we know what happens in the non-optimized case now.

Copy elision

The copy (or move if the class has a move constructor as in my example) can be elided.

§ 12.8 [class.copy] / 31

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects.

So now the question is when does this happen in this example? The reason for the elison of the first copy is given in the very same paragraph:

[...] in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cvunqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value.

Return type matches type in the return statement: function will construct My my; directly into the return value.

The reason for the elison of the second copy/move:

[...] when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move.

Target type matches the type returned by the function: The return value of the function will be constructed into my_in_main.

So you have a cascade here:

My my; in your function is directly constructed into the return value which is directly constructed into my_in_main So you have in fact only one object here and function() would (whatever it does) in fact operate on the object my_in_main.

  1. Call function()
  2. In function construct My instance into my_in_main. Constructor output!
  3. Output stuff. Function output!

my_in_main is still destroyed at the end of main giving a Destructor output!.

That makes three outputs in total: Those you observe if optimization is turned on.

An example where elision is not possible.

In the following example both copies mentioned above cannot be omitted because the class types do not match:

  • The return statement does not match the return type
  • The target type does not match the return type of the function

I just created two additional types:

#include <iostream>
using namespace std;
struct A
{  
   A(void) { cout << "A constructor " << endl; }
  ~A(void) { cout << "A destructor " << endl; }
};
struct B
{  
   B(A const&) { cout << "B copy from A" << endl; }
  ~B(void) { cout << "B destructor " << endl; }
};

struct C
{
   C(B const &) { cout << "C copy from B" << endl; }
  ~C(void) { cout << "C destructor " << endl; }
};

B function() { A my; cout << "function" << endl; return my; }

int main()
{ 
  C my_in_main(function());
  return 0;
}

Here we have the "completely non-optimized behaviour" I mentioned above. I'll refer to the points I've drawn there.

A constructor (see 2.)
function (see 3.)
B copy from A (see 4.)
A destructor (see 5.)
C copy from B (see 6.)
B destructor (see 7.)
C destructor (instance in main, destroy at end of main)
Pixelchemist
  • 24,090
  • 7
  • 47
  • 71