#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.
Taxi
objet à une fonction prenant unCar
objet par valeur?Car
alors ce problème disparaît et il donne les résultats attendus.Réponses:
Il semble que le compilateur Visual Studio prenne un peu de raccourci lors du découpage de votre
taxi
pour 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
taxi
copie et en construit unCar
, 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.
la source
Qu'est-ce qui se passe ?
Lorsque vous créez un
Taxi
, vous créez également unCar
sous - objet. Et lorsque le taxi est détruit, les deux objets sont détruits. Lorsque vous appelez,test()
vous passez laCar
valeur par. Donc une secondeCar
est construite à partir d' une copie et sera détruite quand elletest()
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
Car
créé comme source pour l'Car
argument. Comme cela ne se produit pas lorsque vous fournissez directement uneCar
valeur en argument, je soupçonne que c'est pour transformer leTaxi
enCar
. C'est inattendu, car il y a déjà unCar
sous - objet dans chaqueTaxi
. 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:
[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).Car
valeur. 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.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 duCar
sous - objet deTaxi
.J'ai donc essayé d'appeler
test()
mais de lancer explicitement leTaxi
dans unCar
.Ma première tentative n'a pas réussi à améliorer la situation. Le compilateur utilisait toujours la conversion du constructeur sous-optimal:
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
Car
sous - objet deTaxi
et sans créer cet objet temporaire idiot: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:
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.
la source
Taxi
peut être transmise directement auCar
constructeur de la copie), donc la suppression de la copie n'est pas pertinente.