Warum wurde der Destruktor zweimal ausgeführt?

12
#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;
}

Dies ist die Ausgabe :

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

Ich verwende MS Visual Studio Community 2017 (Entschuldigung, ich weiß nicht, wie ich die Visual C ++ - Edition sehen soll). Als ich den Debug-Modus verwendet habe. Ich finde, ein Destruktor wird ausgeführt, wenn der void test(Car c){ }Funktionskörper wie erwartet verlassen wird. Und ein zusätzlicher Zerstörer erschien, als der vorbei test(taxi);ist.

Die test(Car c)Funktion verwendet den Wert als formalen Parameter. Ein Auto wird kopiert, wenn Sie zur Funktion gehen. Also dachte ich, es wird nur ein "Auto ist zerstört" geben, wenn ich die Funktion verlasse. Aber tatsächlich gibt es zwei "Auto ist zerstört", wenn die Funktion verlassen wird (die erste und zweite Zeile, wie in der Ausgabe gezeigt). Warum gibt es zwei "Auto ist zerstört"? Vielen Dank.

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

Wenn ich class Car zum Beispiel eine virtuelle Funktion hinzufüge : virtual void drive() {} Dann erhalte ich die erwartete Ausgabe.

Car is destructed.
Taxi is destructed.
Car is destructed.
Qiazi
quelle
3
Könnte es ein Problem sein, wie der Compiler mit dem Aufteilen von Objekten umgeht, wenn ein TaxiObjekt an eine Funktion übergeben wird, die ein CarObjekt nach Wert nimmt?
Einige Programmierer Typ
1
Muss Ihr alter C ++ - Compiler sein. g ++ 9 liefert die erwarteten Ergebnisse. Verwenden Sie einen Debugger, um den Grund zu ermitteln, warum eine zusätzliche Kopie des Objekts erstellt wird.
Sam Varshavchik
2
Ich habe g ++ mit Version 7.4.0 und clang ++ mit Version 6.0.0 getestet. Sie gaben die erwartete Ausgabe an, die sich von der Ausgabe von op unterscheidet. Das Problem könnte also der Compiler sein, den er verwendet.
Marceline
1
Ich habe mit MS Visual C ++ reproduziert. Wenn ich einen benutzerdefinierten Kopierkonstruktor und einen Standardkonstruktor für hinzufüge, Carverschwindet dieses Problem und es werden erwartete Ergebnisse erzielt.
Interjay
1
Bitte fügen Sie Compiler und Version zur Frage hinzu
Lightness Races in Orbit

Antworten:

7

Es sieht so aus, als würde der Visual Studio-Compiler beim Schneiden Ihres taxifür den Funktionsaufruf eine kleine Verknüpfung verwenden , was ironischerweise dazu führt, dass er mehr Arbeit erledigt, als man erwarten könnte.

Zuerst wird Ihr taxiund das Kopieren eines Cardavon daraus genommen, damit das Argument übereinstimmt.

Dann wird das Car erneut für den Pass-by-Wert kopiert .

Dieses Verhalten verschwindet, wenn Sie einen benutzerdefinierten Kopierkonstruktor hinzufügen. Der Compiler scheint dies aus eigenen Gründen zu tun (möglicherweise ist es intern ein einfacherer Codepfad), indem er die Tatsache "erlaubt", weil die Kopie selbst ist trivial. Die Tatsache, dass Sie dieses Verhalten immer noch mit einem nicht trivialen Destruktor beobachten können, ist eine kleine Aberration.

Ich weiß nicht, inwieweit dies legal ist (insbesondere seit C ++ 17) oder warum der Compiler diesen Ansatz wählen würde, aber ich würde zustimmen, dass es nicht die Ausgabe ist, die ich intuitiv erwartet hätte. Weder GCC noch Clang tun dies, obwohl es sein kann, dass sie die Dinge auf die gleiche Weise tun, aber dann besser darin sind, die Kopie zu entfernen. Mir ist aufgefallen, dass selbst VS 2019 bei garantierter Elision immer noch nicht großartig ist.

Leichtigkeitsrennen im Orbit
quelle
Entschuldigung, aber ist das nicht genau das, was ich mit "Umwandlung von Taxi in Auto, wenn Ihr Compiler die Kopierentfernung nicht durchführt" gesagt habe.
Christophe
Dies ist eine unfaire Bemerkung, da die Übergabe von Wert und Übergabe von Referenz zur Vermeidung von Slicing nur in einer Bearbeitung hinzugefügt wurde, um OP über diese Frage hinaus zu unterstützen. Dann war meine Antwort kein Schuss in die Dunkelheit, sie wurde von Anfang an klar erklärt, woher sie kommen könnte, und ich bin froh zu sehen, dass Sie zu den gleichen Schlussfolgerungen kommen. Wenn Sie sich nun Ihre Formulierung ansehen: "Es sieht so aus, als ob ... ich weiß nicht", denke ich, dass hier die gleiche Unsicherheit besteht, denn ehrlich gesagt verstehen weder ich noch Sie, warum der Compiler diese Temperatur erzeugen muss.
Christophe
Okay, dann entfernen Sie die nicht verwandten Teile Ihrer Antwort und lassen Sie nur den einzigen verwandten Absatz zurück
Lightness Races in Orbit
Ok, ich habe den ablenkenden Slicing-Para entfernt und den Punkt über die Kopierentfernung mit präzisen Verweisen auf den Standard begründet.
Christophe
Können Sie erklären, warum ein temporäres Auto aus dem Taxi kopiert und dann erneut in den Parameter kopiert werden sollte? Und warum macht der Compiler dies nicht, wenn er mit einem einfachen Auto ausgestattet ist?
Christophe
3

Was ist los ?

Wenn Sie ein erstellen Taxi, erstellen Sie auch ein CarUnterobjekt. Und wenn das Taxi zerstört wird, werden beide Objekte zerstört. Wenn Sie anrufen, übergeben test()Sie den Carby-Wert. Eine Sekunde Carwird also kopiert und zerstört, wenn sie test()übrig bleibt. Wir haben also eine Erklärung für 3 Destruktoren: den ersten und die beiden letzten in der Sequenz.

Der vierte Destruktor (das ist der zweite in der Sequenz) ist unerwartet und ich konnte nicht mit anderen Compilern reproduzieren.

Es kann nur eine temporäre CarQuelle für das CarArgument sein. Da dies nicht der Fall ist, wenn direkt ein CarWert als Argument angegeben wird, vermute ich, dass dies zur Umwandlung des Werts Taxiin dient Car. Dies ist unerwartet, da Carin jedem bereits ein Unterobjekt vorhanden ist Taxi. Daher denke ich, dass der Compiler eine unnötige Konvertierung in eine temporäre Version vornimmt und nicht die Kopierelision ausführt, die diese temporäre Version hätte vermeiden können.

Klarstellung in den Kommentaren:

Hier die Klarstellung unter Bezugnahme auf den Standard für Sprachanwälte zur Überprüfung meiner Ansprüche:

  • Die Konvertierung, auf die ich mich hier beziehe, ist eine Konvertierung durch einen Konstruktor [class.conv.ctor], dh das Konstruieren eines Objekts einer Klasse (hier Auto) basierend auf einem Argument eines anderen Typs (hier Taxi).
  • Diese Konvertierung verwendet dann ein temporäres Objekt, um seinen CarWert zurückzugeben. Der Compiler könnte eine Kopierelision gemäß vornehmen [class.copy.elision]/1.1, da er anstelle einer temporären Konstruktion den Wert konstruieren könnte, der direkt in den Parameter zurückgegeben werden soll.
  • Wenn diese Temperatur Nebenwirkungen hat, liegt dies daran, dass der Compiler diese mögliche Kopierentscheidung anscheinend nicht nutzt. Es ist nicht falsch, da die Kopierentscheidung nicht obligatorisch ist.

Experimentelle Bestätigung der Anaysis

Ich könnte jetzt Ihren Fall mit demselben Compiler reproduzieren und ein Experiment zeichnen, um zu bestätigen, was los ist.

Meine obige Annahme war, dass der Compiler einen suboptimalen Parameterübergabeprozess ausgewählt hat, der die Konstruktorkonvertierung verwendet, Car(const &Taxi)anstatt die Konstruktion direkt aus dem CarUnterobjekt von zu kopieren Taxi.

Also habe ich versucht anzurufen, test()aber das explizit Taxiin a umgewandelt Car.

Mein erster Versuch war nicht erfolgreich, die Situation zu verbessern. Der Compiler verwendete weiterhin die suboptimale Konstruktorkonvertierung:

test(static_cast<Car>(taxi));  // produces the same result with 4 destructor messages

Mein zweiter Versuch war erfolgreich. Es führt auch das Casting durch, verwendet jedoch das Zeiger-Casting, um dem Compiler dringend zu empfehlen, das CarUnterobjekt des zu verwenden, Taxiohne dieses alberne temporäre Objekt zu erstellen:

test(*static_cast<Car*>(&taxi));  //  :-)

Und Überraschung: Es funktioniert wie erwartet und produziert nur 3 Zerstörungsnachrichten :-)

Abschließendes Experiment:

In einem letzten Experiment habe ich einen benutzerdefinierten Konstruktor durch Konvertierung bereitgestellt:

 class Car {
 ... 
     Car(const Taxi& t);  // not necessary but for experimental purpose
 }; 

und implementieren Sie es mit *this = *static_cast<Car*>(&taxi);. Klingt albern, generiert aber auch Code, der nur 3 Destruktormeldungen anzeigt und so das unnötige temporäre Objekt vermeidet.

Dies lässt vermuten, dass im Compiler ein Fehler vorliegt, der dieses Verhalten verursacht. Es ist möglich, dass die Möglichkeit der direkten Erstellung von Kopien aus der Basisklasse unter bestimmten Umständen übersehen wird.

Christophe
quelle
2
Beantwortet die Frage nicht
Leichtigkeitsrennen im Orbit
1
@qiazi Ich denke, dies bestätigt die Hypothese des Temporären für die Konvertierung ohne Kopierelision, da dieses Temporäre aus der Funktion im Kontext des Aufrufers generiert würde.
Christophe
1
Wenn Sie sagen "die Konvertierung von Taxi in Auto, wenn Ihr Compiler die Kopierelision nicht ausführt", auf welche Kopierelision beziehen Sie sich? Es sollte keine Kopie geben, die überhaupt entfernt werden muss.
Interjay
1
@interjay, da der Compiler für die Konvertierung kein temporäres Car basierend auf dem Auto-Unterobjekt von Taxi erstellen und diese Temperatur dann in den Car-Parameter kopieren muss: Er könnte die Kopie entfernen und den Parameter direkt aus dem ursprünglichen Unterobjekt erstellen.
Christophe
1
Die Kopierentfernung erfolgt, wenn der Standard vorschreibt, dass eine Kopie erstellt werden soll, unter bestimmten Umständen jedoch die Elidierung der Kopie möglich ist. In diesem Fall gibt es keinen Grund, überhaupt eine Kopie zu erstellen (ein Verweis auf Taxikann direkt an den CarKopierkonstruktor übergeben werden), sodass die Kopierentscheidung irrelevant ist.
Interjay