#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;
}
esto es salida :
Car is destructed.
Car is destructed.
Taxi is destructed.
Car is destructed.
Uso MS Visual Studio Community 2017 (lo siento, no sé cómo ver la edición de Visual C ++). Cuando usé el modo de depuración. Encuentro que un destructor se ejecuta al dejar el void test(Car c){ }
cuerpo de la función como se esperaba. Y apareció un destructor extra cuando test(taxi);
terminó.
La test(Car c)
función usa el valor como parámetro formal. Se copia un automóvil cuando se va a la función. Así que pensé que solo habrá un "Coche destruido" al salir de la función. Pero en realidad hay dos "El automóvil se destruye" al salir de la función (la primera y la segunda línea como se muestra en la salida) ¿Por qué hay dos "El automóvil se destruye"? Gracias.
===============
cuando agrego una función virtual en, class Car
por ejemplo: virtual void drive() {}
Luego obtengo el resultado esperado.
Car is destructed.
Taxi is destructed.
Car is destructed.
Taxi
objeto a una función que toma unCar
objeto por valor?Car
este problema desaparece y da los resultados esperados.Respuestas:
Parece que el compilador de Visual Studio está tomando un poco de acceso directo cuando corta su
taxi
llamada de función, lo que irónicamente hace que haga más trabajo del que uno podría esperar.Primero, está tomando
taxi
y construyendo una copiaCar
de ella, para que el argumento coincida.Luego, está copiando
Car
nuevamente para el valor de paso.Este comportamiento desaparece cuando agrega un constructor de copia definido por el usuario, por lo que el compilador parece estar haciendo esto por sus propios motivos (quizás, internamente, es una ruta de código más simple), utilizando el hecho de que está "permitido" porque copiar en sí mismo es trivial. El hecho de que aún pueda observar este comportamiento utilizando un destructor no trivial es un poco aberrante.
No sé hasta qué punto esto es legal (particularmente desde C ++ 17), o por qué el compilador tomaría este enfoque, pero estaría de acuerdo en que no es el resultado que habría esperado intuitivamente. Ni GCC ni Clang hacen esto, aunque puede ser que hagan las cosas de la misma manera pero que sean mejores para eludir la copia. Me he dado cuenta de que incluso VS 2019 todavía no es excelente para una elisión garantizada.
fuente
Qué está pasando ?
Cuando crea un
Taxi
, también crea unCar
subobjeto. Y cuando el taxi se destruye, ambos objetos se destruyen. Cuando llamastest()
pasas elCar
por valor. Entonces, un segundoCar
se construye con copia y se destruirá cuandotest()
se deje. Entonces tenemos una explicación para 3 destructores: el primero y los dos últimos en la secuencia.El cuarto destructor (que es el segundo en la secuencia) es inesperado y no pude reproducirlo con otros compiladores.
Solo puede ser un temporal
Car
creado como fuente para elCar
argumento. Como no sucede cuando se proporciona unCar
valor directamente como argumento, sospecho que es para transformarloTaxi
enCar
. Esto es inesperado, ya que ya hay unCar
subobjeto en cada unoTaxi
. Por lo tanto, creo que el compilador realiza una conversión innecesaria en una temperatura y no realiza la copia de elisión que podría haber evitado esta temperatura.Aclaración dada en los comentarios:
Aquí la aclaración con referencia a la norma para el lenguaje-abogado para verificar mis reclamos:
[class.conv.ctor]
, es decir, construir un objeto de una clase (aquí Car) basado en un argumento de otro tipo (aquí Taxi).Car
valor. Se le permitiría al compilador hacer una copia de la elisión de acuerdo con esto[class.copy.elision]/1.1
, ya que en lugar de construir un temporal, podría construir el valor que se devolverá directamente en el parámetro.Confirmación experimental del análisis.
Ahora podría reproducir su caso utilizando el mismo compilador y dibujar un experimento para confirmar lo que está sucediendo.
Mi suposición anterior fue que el compilador seleccionó un proceso de paso de parámetros subóptimos, utilizando la conversión de constructor en
Car(const &Taxi)
lugar de la construcción de copia directamente desde elCar
subobjeto deTaxi
.Así que traté de llamar
test()
pero explícitamente lancé elTaxi
aCar
.Mi primer intento no logró mejorar la situación. El compilador aún usaba la conversión de constructor subóptima:
Mi segundo intento tuvo éxito. También realiza el vaciado, pero usa el vaciado del puntero para sugerir encarecidamente al compilador que use el
Car
subobjeto delTaxi
y sin crear este tonto objeto temporal:Y sorpresa: funciona como se esperaba, produciendo solo 3 mensajes de destrucción :-)
Experimento final:
En un experimento final, proporcioné un constructor personalizado por conversión:
e implementarlo con
*this = *static_cast<Car*>(&taxi);
. Suena tonto, pero esto también genera código que solo mostrará 3 mensajes destructores, evitando así el objeto temporal innecesario.Esto lleva a pensar que podría haber un error en el compilador que causa este comportamiento. Es posible que, en algunas circunstancias, se pierda la posibilidad de construir copias directamente desde la clase base.
fuente
Taxi
se puede pasar directamente alCar
constructor de la copia), por lo que la elisión de la copia es irrelevante.