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.

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.


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.


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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.