Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why was destructor executed twice?

#include <iostream>
using namespace std;

class Car
{
public:
    ~Car()  { cout << "Car is destructed." << endl; }
};

class Taxi :public Car
{
public:
    ~Taxi() {cout << "Taxi is destructed." << endl; }
};

void test(Car c) {}

int main()
{
    Taxi taxi;
    test(taxi);
    return 0;
}

this is output:

Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.

I use MS Visual Studio Community 2017(Sorry, I don't know how to see the Visual C++'s edition). When I used debug mode. I find one destructor is executed when leaving thevoid test(Car c){ } function body as expected. And an extra destructor appeared when the test(taxi);is over.

The test(Car c) function uses value as formal parameter. A Car is copied when going to the function. So I thought there will be only one "Car is destructed" when leaving the function. But actually there are two "Car is destructed" when leaving the function.(the first and second line as showed in the output) Why are there two "Car is destructed"? Thank you.

===============

when I add a virtual function in class Car for example:virtual void drive() {} Then I get the expected output.

Car is destructed.
Taxi is destructed.
Car is destructed.
like image 287
qiazi Avatar asked Oct 27 '19 12:10

qiazi


People also ask

Can a deconstructor return a value?

No, constructor does not return any value. While declaring a constructor you will not have anything like return type. In general, Constructor is implicitly called at the time of instantiation. And it is not a method, its sole purpose is to initialize the instance variables.

Do destructors deallocate memory?

Destructors are usually used to deallocate memory and do other cleanup for a class object and its class members when the object is destroyed. A destructor is called for a class object when that object passes out of scope or is explicitly deleted.

What is the difference between delete and destructor in C++?

Whenever a call to destructor is made , the allocated memory to the object is not released but the object is no longer accessible in the program. But delete completely removes the object from memory.

Do destructors delete pointers?

Default destructors call destructors of member objects, but do NOT delete pointers to objects.


2 Answers

It looks like the Visual Studio compiler is taking a bit of a shortcut when slicing your taxi for the function call, which ironically results in it doing more work than one might expect.

First, it's taking your taxi and copy-constructing a Car from it, so that the argument matches.

Then, it's copying the Car again for the pass-by-value.

This behaviour goes away when you add a user-defined copy constructor, so the compiler seems to be doing this for its own reasons (perhaps, internally, it's a simpler code path), using the fact that it is "allowed" to because the copy itself is trivial. The fact that you can still observe this behaviour using a non-trivial destructor is a bit of an aberration.

I don't know the extent to which this is legal (particularly since C++17), or why the compiler would take this approach, but I would agree that it's not the output I would have intuitively expected. Neither GCC nor Clang do this, though it may be that they do things the same way but are then better at eliding the copy. I have noticed that even VS 2019 is still not great at guaranteed elision.

like image 108
Lightness Races in Orbit Avatar answered Sep 18 '22 03:09

Lightness Races in Orbit


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.

like image 29
Christophe Avatar answered Sep 19 '22 03:09

Christophe