Pourquoi le destructeur a-t-il été exécuté deux fois?

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

c'est la sortie :

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

J'utilise MS Visual Studio Community 2017 (Désolé, je ne sais pas comment voir l'édition de Visual C ++). Quand j'ai utilisé le mode débogage. Je trouve qu'un destructeur est exécuté en quittant le void test(Car c){ }corps de la fonction comme prévu. Et un destructeur supplémentaire est apparu lorsque la fin test(taxi);est terminée.

La test(Car c)fonction utilise la valeur comme paramètre formel. Une voiture est copiée lors de l'accès à la fonction. J'ai donc pensé qu'il n'y aurait qu'une seule "voiture détruite" en quittant la fonction. Mais en réalité, il y a deux "La voiture est détruite" en quittant la fonction. (La première et la deuxième ligne comme indiqué dans la sortie) Pourquoi y a-t-il deux "La voiture est détruite"? Je vous remercie.

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

lorsque j'ajoute une fonction virtuelle class Car par exemple: virtual void drive() {} Ensuite, j'obtiens la sortie attendue.

Car is destructed.
Taxi is destructed.
Car is destructed.
qiazi
la source
3
Pourrait être un problème dans la façon dont le compilateur gère le découpage d'objet lors du passage d'un Taxiobjet à une fonction prenant un Carobjet par valeur?
Un programmeur du
1
Doit être votre ancien compilateur C ++. g ++ 9 donne les résultats attendus. Utilisez un débogueur pour déterminer la raison pour laquelle une copie supplémentaire de l'objet est effectuée.
Sam Varshavchik
2
J'ai testé g ++ avec la version 7.4.0 et clang ++ avec la version 6.0.0. Ils ont donné une sortie attendue qui diffère de celle de l'op. Le problème pourrait donc concerner le compilateur qu'il utilise.
Marceline
1
J'ai reproduit avec MS Visual C ++. Si j'ajoute un constructeur de copie défini par l'utilisateur et un constructeur par défaut pour Caralors ce problème disparaît et il donne les résultats attendus.
interjay
1
Veuillez ajouter le compilateur et la version à la question
Courses de légèreté en orbite

Réponses:

7

Il semble que le compilateur Visual Studio prenne un peu de raccourci lors du découpage de votre taxipour l'appel de fonction, ce qui, ironiquement, lui fait faire plus de travail que ce à quoi on pourrait s'attendre.

Tout d'abord, il prend votre taxicopie et en construit un Car, afin que l'argument corresponde.

Ensuite, il copie à Car nouveau la valeur de passage.

Ce comportement disparaît lorsque vous ajoutez un constructeur de copie défini par l'utilisateur, donc le compilateur semble le faire pour ses propres raisons (peut-être, en interne, c'est un chemin de code plus simple), en utilisant le fait qu'il est «autorisé» à parce que le la copie elle-même est triviale. Le fait que vous puissiez toujours observer ce comportement à l'aide d'un destructeur non trivial est un peu une aberration.

Je ne sais pas dans quelle mesure cela est légal (en particulier depuis C ++ 17), ni pourquoi le compilateur adopterait cette approche, mais je conviens que ce n'est pas la sortie à laquelle je m'attendais intuitivement. Ni GCC ni Clang ne le font, mais il se peut qu'ils fassent les choses de la même manière mais qu'ils réussissent mieux à éluder la copie. Je l' ai remarqué que même VS 2019 est toujours pas grand à élision garanti.

Courses de légèreté en orbite
la source
Désolé, mais n'est-ce pas exactement ce que j'ai dit avec "conversion de Taxi en voiture si votre compilateur ne fait pas l'élision de copie".
Christophe
C'est une remarque injuste, car le passage par valeur vs passage par référence pour éviter le découpage n'a été ajouté que dans une modification, pour aider OP au-delà de cette question. Ensuite, ma réponse n'a pas été un coup de feu dans l'obscurité, elle a été clairement expliquée dès le départ d'où elle peut provenir et je suis heureux de voir que vous arrivez aux mêmes conclusions. Maintenant, en regardant votre formulation, "On dirait ... je ne sais pas", je pense qu'il y a le même degré d'incertitude ici, car franchement ni moi ni vous ne comprenez pourquoi le compilateur a besoin de générer ce temp.
Christophe
D'accord, supprimez les parties non liées de votre réponse en laissant juste le seul paragraphe associé derrière
Courses de légèreté en orbite
Ok, j'ai supprimé le para tranchant distrayant, et j'ai justifié le point concernant la suppression de la copie avec des références précises à la norme.
Christophe
Pourriez-vous expliquer pourquoi une voiture temporaire devrait être construite à partir du taxi puis copiée à nouveau dans le paramètre? Et pourquoi le compilateur ne fait pas cela lorsqu'il est fourni avec une voiture ordinaire?
Christophe
3

Qu'est-ce qui se passe ?

Lorsque vous créez un Taxi, vous créez également un Carsous - objet. Et lorsque le taxi est détruit, les deux objets sont détruits. Lorsque vous appelez, test()vous passez la Carvaleur par. Donc une seconde Carest construite à partir d' une copie et sera détruite quand elle test()sera laissée. Nous avons donc une explication pour 3 destructeurs: le premier et les deux derniers de la séquence.

Le quatrième destructeur (c'est le deuxième de la séquence) est inattendu et je n'ai pas pu reproduire avec d'autres compilateurs.

Il ne peut s'agir que d'un temporaire Carcréé comme source pour l' Carargument. Comme cela ne se produit pas lorsque vous fournissez directement une Carvaleur en argument, je soupçonne que c'est pour transformer le Taxien Car. C'est inattendu, car il y a déjà un Carsous - objet dans chaque Taxi. Par conséquent, je pense que le compilateur effectue une conversion inutile en temp et ne fait pas l'élision de copie qui aurait pu éviter ce temp.

Précision donnée dans les commentaires:

Voici la clarification en référence à la norme pour la langue-avocat pour vérifier mes réclamations:

  • La conversion dont je parle ici, est une conversion par constructeur [class.conv.ctor], c'est-à-dire la construction d'un objet d'une classe (ici Car) sur la base d'un argument d'un autre type (ici Taxi).
  • Cette conversion utilise ensuite un objet temporaire pour renvoyer sa Carvaleur. Le compilateur serait autorisé à effectuer une élision de copie selon [class.copy.elision]/1.1, car au lieu de construire un temporaire, il pourrait construire la valeur à renvoyer directement dans le paramètre.
  • Donc, si ce temp donne des effets secondaires, c'est parce que le compilateur ne fait apparemment pas usage de cette possible élision de copie. Ce n'est pas faux, car la suppression de la copie n'est pas obligatoire.

Confirmation expérimentale de l'anaysis

Je pourrais maintenant reproduire votre cas en utilisant le même compilateur et dessiner une expérience pour confirmer ce qui se passe.

Mon hypothèse ci-dessus était que le compilateur a sélectionné un processus de passage de paramètres sous-optimal, en utilisant la conversion du constructeur Car(const &Taxi)au lieu de copier la construction directement à partir du Carsous - objet de Taxi.

J'ai donc essayé d'appeler test()mais de lancer explicitement le Taxidans un Car.

Ma première tentative n'a pas réussi à améliorer la situation. Le compilateur utilisait toujours la conversion du constructeur sous-optimal:

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

Ma deuxième tentative a réussi. Il effectue également la conversion, mais utilise la conversion de pointeur afin de suggérer fortement au compilateur d'utiliser le Carsous - objet de Taxiet sans créer cet objet temporaire idiot:

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

Et surprise: cela fonctionne comme prévu, ne produisant que 3 messages de destruction :-)

Expérience finale:

Dans une dernière expérience, j'ai fourni un constructeur personnalisé par conversion:

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

et l'implémenter avec *this = *static_cast<Car*>(&taxi);. Cela semble idiot, mais cela génère également du code qui n'affichera que 3 messages de destructeur, évitant ainsi l'objet temporaire inutile.

Cela conduit à penser qu'il pourrait y avoir un bogue dans le compilateur qui provoque ce comportement. C'est af est la possibilité de construction de copie directe à partir de la classe de base serait manquée dans certaines circonstances.

Christophe
la source
2
Ne répond pas à la question
Courses de légèreté en orbite
1
@qiazi Je pense que cela confirme l'hypothèse du temporaire pour la conversion sans élision de copie, car ce temporaire serait généré hors de la fonction, dans le contexte de l'appelant.
Christophe
1
Quand vous dites "la conversion de Taxi en voiture si votre compilateur ne fait pas l'élision de copie", à quelle élision de copie faites-vous référence? Il ne devrait pas y avoir de copie à éluder en premier lieu.
interjay
1
@interjay parce que le compilateur n'a pas besoin de construire un Car temporaire basé sur le sous-objet Taxi de Car pour effectuer la conversion, puis de copier cette température dans le paramètre Car: il pourrait élider la copie et construire directement le paramètre à partir du sous-objet d'origine.
Christophe
1
L'élision de copie est lorsque la norme stipule qu'une copie doit être créée, mais dans certaines circonstances, permet d'éluder la copie. Dans ce cas, il n'y a aucune raison pour qu'une copie soit créée en premier lieu (une référence à Taxipeut être transmise directement au Carconstructeur de la copie), donc la suppression de la copie n'est pas pertinente.
interjay