#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.
Taxi
objeto para uma função que leva umCar
objeto por valor?Car
, esse problema desaparecerá e fornecerá os resultados esperados.Respostas:
Parece que o compilador do Visual Studio está usando um atalho ao cortar sua
taxi
chamada de função, o que ironicamente resulta em mais trabalho do que se poderia esperar.Primeiro, é pegar o seu
taxi
e copiar-construir um aCar
partir 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.
fonte
O que está acontecendo ?
Ao criar um
Taxi
, você também cria umCar
subobjeto. E quando o táxi é destruído, os dois objetos são destruídos. Quando você liga,test()
passa oCar
valor por. Assim, um segundoCar
é copiado e será destruído quandotest()
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
Car
criado como fonte para oCar
argumento. Como isso não acontece ao fornecer diretamente umCar
valor como argumento, suspeito que seja para transformar oTaxi
emCar
. Isso é inesperado, pois já existe umCar
subobjeto em todosTaxi
. 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:
[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).Car
valor. 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.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 doCar
subobjeto deTaxi
.Então, eu tentei ligar,
test()
mas explicitamenteTaxi
converter o em aCar
.Minha primeira tentativa não conseguiu melhorar a situação. O compilador ainda usou a conversão de construtor abaixo do ideal:
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
Car
subobjeto deTaxi
e sem criar este objeto temporário bobo: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:
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.
fonte
Taxi
pode ser passada diretamente para oCar
construtor de cópias); portanto, a eliminação de cópias é irrelevante.