#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.
Taxi
Objekt an eine Funktion übergeben wird, die einCar
Objekt nach Wert nimmt?Car
verschwindet dieses Problem und es werden erwartete Ergebnisse erzielt.Antworten:
Es sieht so aus, als würde der Visual Studio-Compiler beim Schneiden Ihres
taxi
fü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
taxi
und das Kopieren einesCar
davon 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.
quelle
Was ist los ?
Wenn Sie ein erstellen
Taxi
, erstellen Sie auch einCar
Unterobjekt. Und wenn das Taxi zerstört wird, werden beide Objekte zerstört. Wenn Sie anrufen, übergebentest()
Sie denCar
by-Wert. Eine SekundeCar
wird also kopiert und zerstört, wenn sietest()
ü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
Car
Quelle für dasCar
Argument sein. Da dies nicht der Fall ist, wenn direkt einCar
Wert als Argument angegeben wird, vermute ich, dass dies zur Umwandlung des WertsTaxi
in dientCar
. Dies ist unerwartet, daCar
in jedem bereits ein Unterobjekt vorhanden istTaxi
. 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:
[class.conv.ctor]
, dh das Konstruieren eines Objekts einer Klasse (hier Auto) basierend auf einem Argument eines anderen Typs (hier Taxi).Car
Wert 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.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 demCar
Unterobjekt von zu kopierenTaxi
.Also habe ich versucht anzurufen,
test()
aber das explizitTaxi
in a umgewandeltCar
.Mein erster Versuch war nicht erfolgreich, die Situation zu verbessern. Der Compiler verwendete weiterhin die suboptimale Konstruktorkonvertierung:
Mein zweiter Versuch war erfolgreich. Es führt auch das Casting durch, verwendet jedoch das Zeiger-Casting, um dem Compiler dringend zu empfehlen, das
Car
Unterobjekt des zu verwenden,Taxi
ohne dieses alberne temporäre Objekt zu erstellen: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:
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.
quelle
Taxi
kann direkt an denCar
Kopierkonstruktor übergeben werden), sodass die Kopierentscheidung irrelevant ist.