¿Por qué fue destructor ejecutado dos veces?

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

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.
qiazi
fuente
3
¿Podría ser un problema en cómo el compilador maneja el corte de objetos cuando pasa un Taxiobjeto a una función que toma un Carobjeto por valor?
Algún tipo programador el
1
Debe ser tu antiguo compilador de C ++. g ++ 9 da los resultados esperados. Use un depurador para determinar la razón por la cual se realiza una copia adicional del objeto.
Sam Varshavchik
2
He probado g ++ con la versión 7.4.0 y clang ++ con la versión 6.0.0. Dieron la salida esperada que difiere de la salida de op. Entonces, el problema podría ser sobre el compilador que usa.
Marceline
1
Me reproduje con MS Visual C ++. Si agrego un constructor de copia definido por el usuario y un constructor predeterminado para Careste problema desaparece y da los resultados esperados.
Interjay
1
Agregue el compilador y la versión a la pregunta
carreras ligeras en órbita el

Respuestas:

7

Parece que el compilador de Visual Studio está tomando un poco de acceso directo cuando corta su taxillamada de función, lo que irónicamente hace que haga más trabajo del que uno podría esperar.

Primero, está tomando taxiy construyendo una copia Carde 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.

Carreras de ligereza en órbita
fuente
Lo siento, pero ¿no es esto exactamente lo que dije con "conversión de Taxi a Auto si su compilador no hace la copia de elisión"?
Christophe
Esa es una observación injusta, porque el paso por valor frente a paso por referencia para evitar el corte solo se agregó en una edición, para ayudar a OP más allá de esta pregunta. Entonces mi respuesta no fue un tiro en la oscuridad, se explicó claramente desde el principio de dónde puede venir y me alegra ver que llegas a las mismas conclusiones. Ahora, mirando su formulación, "Parece que ... no sé", creo que hay la misma cantidad de incertidumbre aquí, porque francamente ni yo ni tú entendemos por qué el compilador necesita generar esta temperatura.
Christophe
De acuerdo, entonces elimine las partes no relacionadas de su respuesta dejando solo un párrafo relacionado
carreras de ligereza en órbita el
Ok, eliminé el párrafo de corte distractor, y justifiqué el punto sobre la elisión de copia con referencias precisas al estándar.
Christophe
¿Podría explicar por qué un automóvil temporal debe ser construido desde el Taxi y luego copiado nuevamente en el parámetro? ¿Y por qué el compilador no hace esto cuando se le proporciona un automóvil simple?
Christophe
3

Qué está pasando ?

Cuando crea un Taxi, también crea un Carsubobjeto. Y cuando el taxi se destruye, ambos objetos se destruyen. Cuando llamas test()pasas el Carpor valor. Entonces, un segundo Carse construye con copia y se destruirá cuando test()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 Carcreado como fuente para el Carargumento. Como no sucede cuando se proporciona un Carvalor directamente como argumento, sospecho que es para transformarlo Taxien Car. Esto es inesperado, ya que ya hay un Carsubobjeto en cada uno Taxi. 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:

  • La conversión a la que me refiero aquí, es una conversión por constructor [class.conv.ctor], es decir, construir un objeto de una clase (aquí Car) basado en un argumento de otro tipo (aquí Taxi).
  • Esta conversión utiliza entonces un objeto temporal para devolver su Carvalor. 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.
  • Entonces, si esta temperatura produce efectos secundarios, es porque el compilador aparentemente no hace uso de esta posible copia de elisión. No está mal, ya que la elisión de copia no es obligatoria.

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 el Carsubobjeto de Taxi.

Así que traté de llamar test()pero explícitamente lancé el Taxia Car.

Mi primer intento no logró mejorar la situación. El compilador aún usaba la conversión de constructor subóptima:

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

Mi segundo intento tuvo éxito. También realiza el vaciado, pero usa el vaciado del puntero para sugerir encarecidamente al compilador que use el Carsubobjeto del Taxiy sin crear este tonto objeto temporal:

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

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:

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

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.

Christophe
fuente
2
No responde a la pregunta
ligereza corre en órbita el
1
@qiazi Creo que esto confirma la hipótesis del temporal para la conversión sin copia de elisión, porque este temporal se generaría fuera de la función, en el contexto de la persona que llama.
Christophe
1
Al decir "la conversión de Taxi a Auto si su compilador no hace la copia de elisión", ¿a qué copia de elisión se refiere? No debe haber una copia que deba ser eliminada en primer lugar.
Interjay
1
@interjay porque el compilador no necesita construir un Car temporal basado en el sub-objeto Car de Taxi para hacer la conversión y luego copiar esta temperatura en el parámetro Car: podría eludir la copia y construir directamente el parámetro del subobjeto original.
Christophe
1
La elisión de copia es cuando el estándar establece que se debe crear una copia, pero en ciertas circunstancias permite que la copia se elimine. En este caso, no hay ninguna razón para crear una copia en primer lugar (una referencia a Taxise puede pasar directamente al Carconstructor de la copia), por lo que la elisión de la copia es irrelevante.
Interjay