Perché il distruttore è stato giustiziato due volte?


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

questo è output :

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

Uso MS Visual Studio Community 2017 (mi dispiace, non so come vedere l'edizione di Visual C ++). Quando ho usato la modalità debug. Trovo che un distruttore venga eseguito quando si lascia il void test(Car c){ }corpo della funzione come previsto. E un distruttore in più è apparso alla test(taxi);fine.

La test(Car c)funzione utilizza il valore come parametro formale. Un'auto viene copiata quando si accede alla funzione. Quindi ho pensato che ci fosse una sola "Auto distrutta" quando si lascia la funzione. Ma in realtà ci sono due "Auto distrutta" quando si lascia la funzione (la prima e la seconda riga come mostrato nell'output) Perché ci sono due "Auto distrutta"? Grazie.

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

quando aggiungo una funzione virtuale class Car per esempio: virtual void drive() {} Quindi ottengo l'output previsto.

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

3
Potrebbe essere un problema nel modo in cui il compilatore gestisce lo slicing dell'oggetto quando passa un Taxioggetto a una funzione che prende un Caroggetto in base al valore?
Qualche programmatore amico

1
Deve essere il tuo vecchio compilatore C ++. g ++ 9 fornisce i risultati previsti. Utilizzare un debugger per determinare il motivo per cui viene eseguita una copia aggiuntiva dell'oggetto.
Sam Varshavchik,

2
Ho testato g ++ con la versione 7.4.0 e clang ++ con la versione 6.0.0. Hanno fornito l'output previsto che differisce dall'output di op. Quindi il problema potrebbe riguardare il compilatore che usa.
Marceline,

1
Ho riprodotto con MS Visual C ++. Se aggiungo un costruttore di copia definito dall'utente e un costruttore predefinito per Carallora questo problema scompare e dà i risultati previsti.
interjay

1
Aggiungi compilatore e versione alla domanda
Razze di leggerezza in orbita,

Risposte:


7

Sembra che il compilatore di Visual Studio stia prendendo un po 'di una scorciatoia quando si suddivide il tuo taxiper la chiamata di funzione, il che ironicamente lo fa fare più lavoro di quanto ci si potrebbe aspettare.

Innanzitutto, sta prendendo il tuo taxie costruendo una copia Carda esso, in modo che l'argomento corrisponda.

Quindi, sta copiando di Car nuovo il valore pass-by.

Questo comportamento scompare quando si aggiunge un costruttore di copie definito dall'utente, quindi il compilatore sembra farlo per i suoi motivi (forse, internamente, è un percorso di codice più semplice), usando il fatto che è "permesso" perché la copia stessa è banale. Il fatto che sia ancora possibile osservare questo comportamento usando un distruttore non banale è un po 'un'aberrazione.

Non so fino a che punto ciò sia legale (in particolare dal C ++ 17), o perché il compilatore avrebbe adottato questo approccio, ma sarei d'accordo che non è l'output che mi sarei aspettato intuitivamente. Né GCC né Clang fanno questo, anche se può darsi che facciano le cose allo stesso modo ma siano quindi più bravi a eludere la copia. Io ho notato che anche VS 2019 non è ancora grande a elision garantito.


Siamo spiacenti, ma non è esattamente quello che ho detto con la "conversione da Taxi in auto se il tuo compilatore non esegue la copia elisione".
Christophe

Questa è un'osservazione ingiusta, perché il passaggio per valore vs passaggio per refernece per evitare lo slicing è stato aggiunto solo in una modifica, per aiutare OP oltre questa domanda. Quindi la mia risposta non è stata uno sparo nel buio, è stato chiaramente spiegato fin dall'inizio da dove proviene e sono felice di vedere che sei arrivato alle stesse conclusioni. Ora guardando la tua formulazione, "Sembra che ... non lo so", penso che ci sia la stessa quantità di incertezza qui, perché sinceramente né io né te capisci perché il compilatore deve generare questa temperatura.
Christophe,

Va bene quindi rimuovi le parti non correlate della tua risposta lasciando solo il singolo paragrafo correlato
Razze di leggerezza in orbita

Ok, ho rimosso il para distacco distratto e ho giustificato il punto sull'eliminazione della copia con riferimenti precisi allo standard.
Christophe,

Potresti spiegare perché un'auto temporanea dovrebbe essere copiata dal taxi e poi copiata di nuovo nel parametro? E perché il compilatore non lo fa se fornito con una macchina normale?
Christophe,

3

Che cosa sta succedendo ?

Quando si crea un Taxi, si crea anche un Caroggetto secondario. E quando il taxi viene distrutto, entrambi gli oggetti vengono distrutti. Quando chiami test(), passi il Carvalore. Quindi un secondo Carviene copiato e verrà distrutto quandotest() viene lasciato. Quindi abbiamo una spiegazione per 3 distruttori: il primo e i due ultimi della sequenza.

Il quarto distruttore (che è il secondo nella sequenza) è inaspettato e non ho potuto riprodurlo con altri compilatori.

Può essere solo un temporaneo Carcreato come fonte per l' Carargomento. Dal momento che non accade quando si fornisce direttamente un Carvalore come argomento, ho il sospetto che sia per trasformarlo Taxiin Car. Ciò è inaspettato, poiché esiste già un Caroggetto secondario in ogni Taxi. Pertanto, penso che il compilatore non effettui una conversione non necessaria in una temp e non esegua la copia elisione che avrebbe potuto evitare questa temp.

Chiarimento fornito nei commenti:

Qui il chiarimento con riferimento alla norma per l'avvocato linguista per verificare le mie affermazioni:

  • La conversione a cui mi riferisco qui è una conversione per costruttore [class.conv.ctor], ovvero la costruzione di un oggetto di una classe (qui Car) basato su un argomento di altro tipo (qui Taxi).
  • Questa conversione utilizza quindi un oggetto temporaneo per restituire il suo Carvalore. Al compilatore sarebbe consentito effettuare una copia elisione secondo [class.copy.elision]/1.1, poiché invece di costruire un temporaneo, potrebbe costruire il valore da restituire direttamente nel parametro.
  • Quindi, se questa temperatura produce effetti collaterali, è perché il compilatore apparentemente non utilizza questa possibile copia-elisione. Non è sbagliato, dal momento che l'elezione della copia non è obbligatoria.

Conferma sperimentale dell'analisi

Ora potrei riprodurre il tuo caso usando lo stesso compilatore e disegnare un esperimento per confermare cosa sta succedendo.

La mia ipotesi di cui sopra era che il compilatore ha selezionato un parametro non ottimale passando il processo, usando la conversione del costruttore Car(const &Taxi)invece della copia della costruzione direttamente dal Caroggetto secondario di Taxi.

Quindi ho provato a chiamare test()ma esplicitamente lanciando il file Taxia Car.

Il mio primo tentativo non è riuscito a migliorare la situazione. Il compilatore utilizzava ancora la conversione del costruttore non ottimale:

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

Il mio secondo tentativo è riuscito. Fa anche il casting, ma usa il pointer pointer per suggerire fortemente al compilatore di usare l' Caroggetto secondario Taxidell'oggetto e senza creare questo stupido oggetto temporaneo:

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

E sorpresa: funziona come previsto, producendo solo 3 messaggi di distruzione :-)

Esperimento conclusivo:

In un esperimento finale, ho fornito un costruttore personalizzato per conversione:

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

e implementarlo con *this = *static_cast<Car*>(&taxi);. Sembra sciocco, ma questo genera anche codice che visualizzerà solo 3 messaggi di distruttore, evitando così l'oggetto temporaneo non necessario.

Questo porta a pensare che potrebbe esserci un bug nel compilatore che causa questo comportamento. È una possibilità che in alcune circostanze manchi la possibilità di costruire copie dirette dalla classe base.


2
Non risponde alla domanda
gare di leggerezza in orbita

1
@qiazi Penso che ciò confermi l'ipotesi del temporaneo per la conversione senza elisione della copia, perché questo temporaneo sarebbe generato dalla funzione, nel contesto del chiamante.
Christophe,

1
Quando dici "la conversione da Taxi in Auto se il tuo compilatore non esegue la copia elisione", a quale copia della copia ti riferisci? Non ci dovrebbero essere copie che devono essere eluse in primo luogo.
interjay

1
@interjay perché il compilatore non ha bisogno di costruire un temporaneo Car basato sul sub-oggetto Car di Taxi per fare la conversione e quindi copiare questo temp nel parametro Car: potrebbe eludere la copia e costruire direttamente il parametro dal oggetto secondario originale.
Christophe,

1
Copia elisione è quando lo standard afferma che è necessario creare una copia, ma in determinate circostanze consente la copia. In questo caso non vi è alcun motivo per cui una copia debba essere creata in primo luogo (un riferimento a Taxipuò essere passato direttamente al Carcostruttore della copia), quindi l'eliminazione della copia è irrilevante.
interjay
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.