78 votes

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

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

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

Comme aucun destructeur n'est défini, un compilateur C++ devrait 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 que nous pouvons raisonnablement compter sur le destructeur que le compilateur nous fournit), la définition d'un destructeur vide, c'est-à-dire .

Foo::~Foo() { }

fait 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ù se situent-elles ? Dans le cas contraire, une méthode est-elle préférée à l'autre ?

0 votes

J'ai un peu modifié cette question pour que l'édition de l'afterthoguht devienne une partie réelle de la question. S'il y a des erreurs de syntaxe dans les parties que j'ai modifiées, criez sur moi, pas sur l'auteur de la question originale. @Andrew, si vous avez l'impression que j'ai trop modifié votre question, n'hésitez pas à revenir en arrière ; si vous aimez le changement mais pensez que ce n'est pas suffisant, vous êtes évidemment le bienvenu pour modifier votre propre question.

121voto

Johannes Schaub - litb Points 256113

Il fera la même chose (rien, en gros). Mais ce n'est pas la même chose que si vous ne l'aviez pas écrit. Parce que pour écrire le destructeur, il faut que le destructeur de la classe de base fonctionne. Si le destructeur de la 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érez ceci

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

C'est OK, tant que vous n'avez pas besoin de déstructurer un objet de type B (et donc, implicitement, de type A) - comme si vous n'appelez jamais delete sur un objet créé dynamiquement, ou si vous ne créez jamais un objet de ce type en premier lieu. Si vous le faites, alors le compilateur affichera un diagnostic approprié. Maintenant, si vous en fournissez un explicitement

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

Celle-ci tentera d'appeler implicitement le destructeur de la classe de base, et provoquera un diagnostic déjà 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érons ce membre pointeur intelligent

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

Supposons que l'objet de type C est créé dans la définition du constructeur de A dans le fichier .cpp qui contient également la définition de la structure C . Maintenant, si vous utilisez la structure A et exiger la destruction d'un A le compilateur fournira une définition implicite du destructeur, comme dans le cas ci-dessus. Ce destructeur appellera aussi implicitement le destructeur de l'objet auto_ptr. Et cela supprimera le pointeur qu'il détient, qui pointe sur l'objet C sans connaître la définition de l'objet C ! Cela est apparu dans le .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 de fournir une définition vide de celui-ci dans le fichier .cpp où la structure C est défini. Au moment où il invoquera le destructeur de son membre, il connaîtra alors la définition de la structure C et peut 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 exige au contraire 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 lorsque 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 PODs (plain old data), et ils ne sont pas autorisés à être copiés en bits. Notez que cette restriction n'est pas vraiment nécessaire - et la prochaine version de C++ a amélioré la situation à ce sujet, de sorte qu'elle vous permet de copier en bits de tels types, tant que d'autres changements plus importants ne sont pas effectués.


Puisque vous avez demandé des constructeurs : Eh bien, pour ceux-ci, 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 à l'exécution - la pure possibilité compte déjà ici) feront le même mal que pour les destructeurs, et se produisent lorsque quelque chose dans le constructeur jette - le compilateur est alors tenu d'appeler le destructeur des membres. Cette réponse fait un certain usage de la définition implicite des constructeurs par défaut.

De plus, la même chose est vraie pour la visibilité et la PODness que ce que j'ai dit à propos 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 valeur des membres, et c'est à votre constructeur de faire toute 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 non défini, car b n'a jamais été initialisé (votre constructeur l'a omis). La valeur peut être zéro, mais peut tout aussi bien être n'importe quelle autre valeur bizarre. Essayer de lire à partir d'un tel objet non initialisé provoque un comportement non défini.

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

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

0 votes

+1 pour la mention des pointeurs automatiques déclarés à l'avance et du destructeur automatique. C'est une erreur fréquente quand on commence à déclarer des choses.

1 votes

Votre premier exemple est un peu étrange. Le B que vous avez écrit ne peut pas être utilisé du tout (en créer un serait une erreur, tout cast vers un serait un comportement indéfini, puisque ce n'est pas un POD).

0 votes

De même, A().a == 0 n'est vrai que pour les statiques. Une variable locale de type A ne sera pas initialisée.

18voto

Gregory Pakosz Points 35546

Je sais que j'interviens tardivement dans la discussion, mais d'après mon expérience, le compilateur se comporte différemment face à un destructeur vide et face à 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 examinant l'assemblage généré pour un code utilisant des modèles d'expression, je me suis rendu compte qu'en mode "release", l'appel à mon fichier BinaryVectorExpression operator + (const Vector& lhs, const Vector& rhs) n'a jamais été inlined. (ne faites pas attention aux types exacts et à la signature des opérateurs).

Pour diagnostiquer plus précisément le problème, j'ai activé les différents éléments suivants Avertissements du compilateur qui sont désactivés par défaut . Le site C4714 est particulièrement intéressant. Il est émis par le compilateur lorsqu'une fonction marquée du symbole __forceinline n'est pas inlined néanmoins .

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 mettre en ligne l'appel à l'opérateur.

Parmi les raisons décrites dans la documentation, le compilateur ne réussit pas à mettre en ligne une fonction marquée par le symbole __forceinline pour :

Fonctions qui renvoient un objet indéformable par valeur lorsque -GX/EHs/EHa est activé

C'est le cas de mon 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 indétrônable. Ajout de throw () au destructeur n'a pas aidé le compilateur et J'évite d'utiliser les spécifications des exceptions de toute façon . La mise en commentaire du destructeur vide a permis au compilateur d'intégrer complètement le code.

La leçon à retenir est qu'à partir de maintenant, dans chaque cours, j'écris des destructeurs vides commentés pour faire savoir aux humains 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() */ pour indiquer que le destructeur ne peut pas lancer.

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

J'espère que cela vous aidera.

12voto

Faisal Vali Points 10048

Le destructeur vide que vous avez défini en dehors de la classe a une sémantique similaire dans la plupart des cas, mais pas dans 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énoté comme un destructeur trivial (nécessaire pour faire des types triviaux qui peuvent être dans des unions, les vôtres ne le peuvent pas).
3) a une spécification d'exception (throw(), la vôtre ne l'a pas)

1 votes

Une note sur 3 : La spécification de l'exception n'est pas toujours vide dans un destructeur implicitement défini, comme indiqué dans [except.spec].

0 votes

@dalle +1 sur le commentaire - merci d'avoir attiré l'attention sur ce point - vous avez effectivement raison, si Foo avait dérivé de classes de base ayant chacune des destructeurs non-implicites avec des spécifications d'exception - le destructeur implicite de Foo aurait "hérité" de l'union de ces spécifications d'exception - dans ce cas, puisqu'il n'y a pas d'héritage, la spécification d'exception du destructeur implicite se trouve être throw().

9voto

David Seiler Points 6212

Oui, ce destructeur vide est le même que celui qui est 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 le destructeur explicitement, sauf si vous devez faire quelque chose d'inhabituel : le rendre virtuel ou privé, par exemple.

3voto

oscarkuo Points 5849

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() { }

l'absence de destructeur virtuel peut entraîner une fuite de mémoire car les personnes qui héritent de votre classe Foo n'ont peut-être pas remarqué que leur destructeur ne sera jamais appelé !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X