What is happening ?
When you create a Taxi
, you also create a Car
subobject. And when the taxi gets destroyed, both objects are destructed. When you call test()
you pass the Car
by value. So a second Car
gets copy-constructed and will get destructed when test()
is left. So we have an explanation for 3 destructors: the first and the two last in the sequence.
The fourth destructor (that is the second in the sequence) is unexpected and I couldn't reproduce with other compilers.
It can only be a temporary Car
created as source for the Car
argument. Since it doesn't happen when providing directly a Car
value as argument, I suspect it is for transforming the Taxi
into Car
. This is unexpected, since there is already a Car
subobject in every Taxi
. Therefore I think that the compiler does make an unnecessary conversion into a temp and doesn't do the copy elision that could have avoided this temp.
Clarification given in the comments:
Here the clarification with reference to the standard for language-lawyer to verify my claims:
- The conversion I am referring to here, is a conversion by constructor
[class.conv.ctor]
, i.e. constructing an object of one class (here Car) based on an argument of another type (here Taxi).
- This conversion uses then a temporary object for returning its
Car
value. The compiler would be allowed to make a copy elision according [class.copy.elision]/1.1
, since instead of constructing a temporary, it could construct the value to be returned directly into the parameter.
- So if this temp gives side-effects, it's because the compiler apparently doesn't make use of this possible copy-elision. It's not wrong, since copy elision is not mandatory.
Experimental confirmation of the anaysis
I could now reproduce your case by using the same compiler and draw an experiment to confirm what is going on.
My assumption above was that the compiler selected a suboptimal parameter passing process, using the constructor conversion Car(const &Taxi)
instead of copy constructing directly from the Car
subobject of Taxi
.
So I tried calling test()
but explicitly casting the Taxi
into a Car
.
My first attempt did not succeed to improve the situation. The compiler still used the suboptimal constructor conversion:
test(static_cast<Car>(taxi)); // produces the same result with 4 destructor messages
My second attempt succeeded. It does the casting as well, but uses pointer casting in order to strongly suggest the compiler to use the Car
subobject of the Taxi
and without creating this silly temporary object:
test(*static_cast<Car*>(&taxi)); // :-)
And surprise: it works as expected, producting only 3 destruction message :-)
Concluding experiment:
In a final experiment, I provided a custom constructor by conversion:
class Car {
...
Car(const Taxi& t); // not necessary but for experimental purpose
};
and implement it with *this = *static_cast<Car*>(&taxi);
. Sounds silly, but this also generates code that will only display 3 destructor messages, thus avoiding the unnecessary temporary object.
This leads to think that there could be a bug in the compiler that causes this behavior. It is af is the possibility of direct copy-constructing from the base class would be missed in some circumstances.
Taxi
object to a function taking aCar
object by value? – TubuleCar
then this issue disappears and it gives expected results. – AdonCar.class
would be Java. This is not Java. – Arnetest(*static_cast<Car*>(&taxi))
results in exactly 3 destructor message whereastest(static_cast<Car>(taxi))
still results in ' destructor messages, confirming that the compiler does not detect the best way to get a Car from a Taxi. – Weathered