Por que o destruidor foi executado duas vezes?

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

isto é saída :

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

Uso a Comunidade do MS Visual Studio 2017 (Desculpe, não sei como ver a edição do Visual C ++). Quando eu usei o modo de depuração. Acho que um destruidor é executado ao deixar o void test(Car c){ }corpo da função como esperado. E um destruidor extra apareceu quando test(taxi);acabou.

A test(Car c)função usa o valor como parâmetro formal. Um carro é copiado ao ir para a função. Então eu pensei que haveria apenas um "carro destruído" ao sair da função. Mas, na verdade, existem dois "O carro está destruído" ao sair da função (a primeira e a segunda linha, como mostrado na saída) Por que existem dois "O carro está destruído"? Obrigado.

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

quando adiciono uma função virtual, class Car por exemplo: virtual void drive() {} Em seguida, recebo a saída esperada.

Car is destructed.
Taxi is destructed.
Car is destructed.
qiazi
fonte
3
Poderia ser um problema em como o compilador lida com o fatiamento de objeto ao passar um Taxiobjeto para uma função que leva um Carobjeto por valor?
Algum programador cara
11
Deve ser seu antigo compilador C ++. O g ++ 9 fornece os resultados esperados. Use um depurador para determinar o motivo pelo qual uma cópia extra do objeto é feita.
Sam Varshavchik 27/10/19
2
Eu testei o g ++ com a versão 7.4.0 e clang ++ com a versão 6.0.0. Eles deram a saída esperada que difere da saída da operação. Portanto, o problema pode ser sobre o compilador que ele usa.
Marceline
11
Eu reproduzi com o MS Visual C ++. Se eu adicionar um construtor de cópias e construtor padrão definido pelo usuário Car, esse problema desaparecerá e fornecerá os resultados esperados.
interjay
11
Por favor, adicione compilador e versão à pergunta
Lightness Races in Orbit

Respostas:

7

Parece que o compilador do Visual Studio está usando um atalho ao cortar sua taxichamada de função, o que ironicamente resulta em mais trabalho do que se poderia esperar.

Primeiro, é pegar o seu taxie copiar-construir um a Carpartir dele, para que o argumento corresponda.

Em seguida, ele é copiado Car novamente para o valor de passagem.

Esse comportamento desaparece quando você adiciona um construtor de cópias definido pelo usuário; portanto, o compilador parece estar fazendo isso por seus próprios motivos (talvez, internamente, seja um caminho de código mais simples), usando o fato de que "é permitido", porque o copiar em si é trivial. O fato de você ainda poder observar esse comportamento usando um destruidor não trivial é um pouco aberrante.

Não sei até que ponto isso é legal (principalmente desde o C ++ 17), ou por que o compilador adotaria essa abordagem, mas eu concordaria que não é a saída que eu esperaria intuitivamente. Nem o GCC nem o Clang fazem isso, embora possa ser que eles façam as coisas da mesma maneira, mas depois sejam melhores em excluir a cópia. Eu tenho notado que mesmo VS 2019 ainda não é grande na elisão garantida.

Raças de leveza em órbita
fonte
Desculpe, mas não foi exatamente isso que eu disse com "conversão de táxi em carro, se o seu compilador não executar a cópia".
Christophe
Essa é uma observação injusta, porque a passagem por valor versus passagem por referência para evitar o fatiamento foi adicionada apenas em uma edição, para ajudar o OP a sair dessa questão. Então minha resposta não foi um tiro no escuro, foi explicado claramente desde o início de onde pode vir e fico feliz em ver que você chegou às mesmas conclusões. Agora, olhando para a sua formulação, "Parece que ... eu não sei", acho que há a mesma quantidade de incerteza aqui, porque, francamente, nem eu nem você entendemos por que o compilador precisa gerar essa temperatura.
Christophe
Ok, então remover as partes não relacionados de sua resposta, deixando apenas o único parágrafo relacionado trás
Leveza raças na órbita
Ok, eu removi o parágrafo de fatiar que distrai e justifiquei o ponto sobre a elisão de cópias com referências precisas ao padrão.
Christophe
Você poderia explicar por que um carro temporário deve ser copiado do táxi e copiado novamente para o parâmetro? E por que o compilador não faz isso quando fornecido com um carro comum?
Christophe
3

O que está acontecendo ?

Ao criar um Taxi, você também cria um Carsubobjeto. E quando o táxi é destruído, os dois objetos são destruídos. Quando você liga, test()passa o Carvalor por. Assim, um segundo Caré copiado e será destruído quando test()for deixado. Portanto, temos uma explicação para três destruidores: o primeiro e os dois últimos na sequência.

O quarto destruidor (que é o segundo na sequência) é inesperado e não consegui reproduzir com outros compiladores.

Só pode ser temporário Carcriado como fonte para o Carargumento. Como isso não acontece ao fornecer diretamente um Carvalor como argumento, suspeito que seja para transformar o Taxiem Car. Isso é inesperado, pois já existe um Carsubobjeto em todos Taxi. Portanto, acho que o compilador faz uma conversão desnecessária em um temp e não faz a cópia elision que poderia ter evitado esse temp.

Esclarecimentos dados nos comentários:

Aqui o esclarecimento com referência ao padrão para o advogado de idiomas para verificar minhas reivindicações:

  • A conversão a que me refiro aqui é uma conversão por construtor [class.conv.ctor], ou seja, a construção de um objeto de uma classe (aqui Carro) com base em um argumento de outro tipo (aqui Táxi).
  • Essa conversão usa um objeto temporário para retornar seu Carvalor. O compilador poderia fazer uma cópia elisão de acordo [class.copy.elision]/1.1, pois, em vez de construir um temporário, ele poderia construir o valor a ser retornado diretamente no parâmetro.
  • Portanto, se esta temperatura fornecer efeitos colaterais, é porque o compilador aparentemente não faz uso dessa possível cópia elisão. Não está errado, uma vez que a remoção de cópias não é obrigatória.

Confirmação experimental da análise

Agora eu poderia reproduzir seu caso usando o mesmo compilador e desenhar um experimento para confirmar o que está acontecendo.

Minha suposição acima foi que o compilador selecionou um processo de passagem de parâmetro abaixo do ideal, usando a conversão do construtor em Car(const &Taxi)vez de copiar a construção diretamente do Carsubobjeto de Taxi.

Então, eu tentei ligar, test()mas explicitamente Taxiconverter o em a Car.

Minha primeira tentativa não conseguiu melhorar a situação. O compilador ainda usou a conversão de construtor abaixo do ideal:

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

Minha segunda tentativa foi bem sucedida. Ele também executa a conversão, mas usa a conversão de ponteiro para sugerir fortemente que o compilador use o Carsubobjeto de Taxie sem criar este objeto temporário bobo:

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

E surpresa: funciona como esperado, produzindo apenas 3 mensagens de destruição :-)

Experiência final:

Em uma experiência final, forneci um construtor personalizado por conversão:

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

e implementá-lo com *this = *static_cast<Car*>(&taxi);. Parece bobo, mas isso também gera código que exibirá apenas três mensagens destruidoras, evitando assim o objeto temporário desnecessário.

Isso leva a pensar que poderia haver um erro no compilador que causa esse comportamento. É uma possibilidade de que a construção direta de cópias da classe base seja perdida em algumas circunstâncias.

Christophe
fonte
2
Não responde à pergunta
Lightness Races in Orbit
11
@ qiazi Acho que isso confirma a hipótese do temporário para conversão sem elisão de cópia, porque esse temporário seria gerado fora da função, no contexto do chamador.
Christophe
11
Ao dizer "a conversão de táxi para carro se o seu compilador não faz a cópia elision", a que cópia você está se referindo? Não deve haver cópia que precise ser elidida em primeiro lugar.
interjay
11
@interjay porque o compilador não precisa construir um temporário Car com base no subobjeto Car do táxi para fazer a conversão e depois copiar esta temperatura no parâmetro Car: ele pode excluir a cópia e construir diretamente o parâmetro a partir do subobjeto original.
Christophe
11
A elisão da cópia é quando o padrão declara que uma cópia deve ser criada, mas sob certas circunstâncias permite que a cópia seja elidida. Nesse caso, não há motivo para criar uma cópia em primeiro lugar (uma referência Taxipode ser passada diretamente para o Carconstrutor de cópias); portanto, a eliminação de cópias é irrelevante.
interjay