web-dev-qa-db-fra.com

Un constructeur ou destructeur «vide» fera-t-il la même chose que celui généré?

Supposons que nous ayons une classe (jouet) C++ telle que la suivante:

class Foo {
    public:
        Foo();
    private:
        int t;
};

Comme aucun destructeur n'est défini, un compilateur C++ doit en créer un automatiquement pour la classe Foo. Si le destructeur n'a pas besoin de nettoyer la mémoire allouée dynamiquement (c'est-à-dire, nous pourrions raisonnablement compter sur le destructeur que le compilateur nous donne), définira un destructeur vide, c'est-à-dire.

Foo::~Foo() { }

faire la même chose que celle générée par le compilateur? Qu'en est-il d'un constructeur vide - c'est-à-dire Foo::Foo() { }?

S'il y a des différences, où existent-elles? Sinon, une méthode est-elle préférée à l'autre?

72
Andrew Song

Il fera la même chose (rien, en substance). Mais ce n'est pas la même chose que si vous ne l'avez pas écrit. Parce que l'écriture du destructeur nécessitera un destructeur de classe de base fonctionnel. Si le destructeur de classe de base est privé ou s'il y a une autre raison pour laquelle il ne peut pas être invoqué, alors votre programme est défectueux. Considère ceci

struct A { private: ~A(); };
struct B : A { }; 

C'est OK, tant que vous n'avez pas besoin de détruire un objet de type B (et donc implicitement de type A) - comme si vous n'appelez jamais delete sur un objet créé dynamiquement, ou que vous n'en créez jamais un objet dans la première place. Si vous le faites, le compilateur affichera un diagnostic approprié. Maintenant, si vous en fournissez un explicitement

struct A { private: ~A(); };
struct B : A { ~B() { /* ... */ } }; 

Celui-ci tentera d'appeler implicitement le destructeur de la classe de base, et provoquera déjà un diagnostic au moment de la définition de ~B.

Il existe une autre différence qui concerne la définition du destructeur et les appels implicites aux destructeurs membres. Considérez ce membre de pointeur intelligent

struct C;
struct A {
    auto_ptr<C> a;
    A();
};

Supposons que l'objet de type C soit créé dans la définition du constructeur de A dans le fichier .cpp, Qui contient également la définition de struct C. Maintenant, si vous utilisez struct A et que vous avez besoin de détruire un objet A, le compilateur fournira une définition implicite du destructeur, comme dans le cas ci-dessus. Ce destructeur appellera également implicitement le destructeur de l'objet auto_ptr. Et cela supprimera le pointeur qu'il contient, qui pointe vers l'objet C - sans connaître la définition de C! Cela est apparu dans le fichier .cpp Où le constructeur de la structure A est défini.

Il s'agit en fait d'un problème courant dans la mise en œuvre de l'idiome pimpl. La solution ici est d'ajouter un destructeur et d'en fournir une définition vide dans le fichier .cpp, Où la structure C est définie. Au moment où il invoque le destructeur de son membre, il connaîtra alors la définition de struct C et pourra appeler correctement son destructeur.

struct C;
struct A {
    auto_ptr<C> a;
    A();
    ~A(); // defined as ~A() { } in .cpp file, too
};

Notez que boost::shared_ptr N'a pas ce problème: il requiert à la place un type complet lorsque son constructeur est invoqué de certaines manières.

Un autre point où cela fait une différence dans le C++ actuel est quand vous voulez utiliser memset et amis sur un tel objet qui a un destructeur déclaré par l'utilisateur. De tels types ne sont plus des POD (anciennes données simples), et ceux-ci ne peuvent pas être copiés en bits. Notez que cette restriction n'est pas vraiment nécessaire - et la prochaine version C++ a amélioré la situation à ce sujet, de sorte qu'elle vous permet de copier des bits de tels types, tant que d'autres modifications plus importantes ne sont pas apportées.


Depuis que vous avez demandé des constructeurs: Eh bien, pour ces choses, les mêmes choses sont vraies. Notez que les constructeurs contiennent également des appels implicites aux destructeurs. Sur des choses comme auto_ptr, ces appels (même s'ils ne sont pas réellement effectués au moment de l'exécution - la pure possibilité compte déjà ici) feront le même mal que pour les destructeurs, et se produiront lorsque quelque chose dans le constructeur lancera - le compilateur est alors requis d'appeler le destructeur des membres. Cette réponse utilise une définition implicite des constructeurs par défaut.

En outre, la même chose est vraie pour la visibilité et le PODness que j'ai dit au sujet du destructeur ci-dessus.

Il y a une différence importante concernant l'initialisation. Si vous mettez un constructeur déclaré par l'utilisateur, votre type ne reçoit plus d'initialisation de la valeur des membres, et c'est à votre constructeur de faire l'initialisation nécessaire. Exemple:

struct A {
    int a;
};

struct B {
    int b;
    B() { }
};

Dans ce cas, ce qui suit est toujours vrai

assert(A().a == 0);

Alors que ce qui suit est un comportement indéfini, car b n'a jamais été initialisé (votre constructeur l'a omis). La valeur peut être nulle, mais peut également être toute autre valeur étrange. Essayer de lire à partir d'un tel objet non initialisé provoque un comportement indéfini.

assert(B().b == 0);

Cela est également vrai pour l'utilisation de cette syntaxe dans new, comme new A() (notez les parenthèses à la fin - si elles sont omises, l'initialisation de la valeur n'est pas effectuée, et puisqu'il n'y a pas de constructeur déclaré par l'utilisateur qui pourrait l'initialiser, a ne sera pas initialisé).

116

Je sais que je suis en retard dans la discussion, néanmoins mon expérience montre que le compilateur se comporte différemment face à un destructeur vide par rapport à un destructeur généré par le compilateur. C'est du moins le cas avec MSVC++ 8.0 (2005) et MSVC++ 9.0 (2008).

En regardant l'assembly généré pour du code utilisant des modèles d'expression, je me suis rendu compte qu'en mode de libération, l'appel à ma BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) n'était jamais en ligne. (veuillez ne pas faire attention aux types exacts et à la signature de l'opérateur).

Pour diagnostiquer davantage le problème, j'ai activé les divers avertissements du compilateur désactivés par défaut . L'avertissement C4714 est particulièrement intéressant. Il est émis par le compilateur lorsqu'une fonction marquée avec __forceinline n'est pas néanmoins alignée .

J'ai activé l'avertissement C4714 et j'ai marqué l'opérateur avec __forceinline Et j'ai pu vérifier que le compilateur signale qu'il n'a pas pu incorporer l'appel à l'opérateur.

Parmi les raisons décrites dans la documentation, le compilateur ne parvient pas à aligner une fonction marquée avec __forceinline Pour:

Fonctions renvoyant un objet déroulable par valeur lorsque -GX/EHs/EHa est activé

C'est le cas de ma BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs). BinaryVectorExpression est retourné par valeur et même si son destructeur est vide, il fait que cette valeur de retour est considérée comme un objet déroulable. L'ajout de throw () au destructeur n'a pas aidé le compilateur et j'évite de toute façon d'utiliser les spécifications d'exception . En commentant le destructeur vide, le compilateur insère complètement le code.

La conséquence est qu'à partir de maintenant, dans chaque classe, j'écris des destructeurs vides commentés pour que les humains sachent que le destructeur ne fait rien exprès, de la même manière que les gens commentent la spécification d'exception vide `/ * throw () */to indiquent que le destructeur ne peut pas lancer.

//~Foo() /* throw() */ {}

J'espère que cela pourra aider.

17
Gregory Pakosz

Le destructeur vide que vous avez défini hors classe a une sémantique similaire à la plupart des égards, mais pas à tous.

Plus précisément, le destructeur implicitement défini
1) est un en ligne membre public (le vôtre n'est pas en ligne)
2) est désigné comme un destructeur trivial (nécessaire pour créer des types triviaux qui peuvent être en union, le vôtre ne peut pas)
3) a une spécification d'exception (throw (), pas la vôtre)

12
Faisal Vali

Oui, ce destructeur vide est le même que celui généré automatiquement. J'ai toujours laissé le compilateur les générer automatiquement; Je ne pense pas qu'il soit nécessaire de spécifier explicitement le destructeur, sauf si vous devez faire quelque chose d'inhabituel: le rendre virtuel ou privé, par exemple.

8
David Seiler

Je suis d'accord avec David sauf que je dirais que c'est généralement une bonne pratique de définir un destructeur virtuel, c'est-à-dire.

virtual ~Foo() { }

manquer le destructeur virtuel peut entraîner une fuite de mémoire car les personnes qui héritent de votre classe Foo peuvent ne pas avoir remarqué que leur destructeur ne sera jamais appelé !!

3
oscarkuo

Je dirais qu'il vaut mieux mettre la déclaration vide, elle indique à tous les futurs responsables que ce n'était pas une erreur, et vous vouliez vraiment utiliser celle par défaut.

1
Ape-inago

Une définition vide est très bien car la définition peut être référencée

virtual ~GameManager() { };
virtuel ~ GameManager ();
pas de définition pour destructeur virtuel
Undefined symbols:
  "vtable for GameManager", referenced from:
      __ZTV11GameManager$non_lazy_ptr in GameManager.o
      __ZTV11GameManager$non_lazy_ptr in Main.o
ld: symbol(s) not found
0
MLRUS