GNU GCC (g ++): Pourquoi génère-t-il plusieurs dtors?

90

Environnement de développement: GNU GCC (g ++) 4.1.2

Alors que j'essaie d'étudier comment augmenter la `` couverture du code - en particulier la couverture des fonctions '' dans les tests unitaires, j'ai constaté qu'une partie de la classe dtor semble être générée plusieurs fois. Certains d'entre vous ont-ils une idée de pourquoi, s'il vous plaît?

J'ai essayé et observé ce que j'ai mentionné ci-dessus en utilisant le code suivant.

Dans "test.h"

class BaseClass
{
public:
    ~BaseClass();
    void someMethod();
};

class DerivedClass : public BaseClass
{
public:
    virtual ~DerivedClass();
    virtual void someMethod();
};

Dans "test.cpp"

#include <iostream>
#include "test.h"

BaseClass::~BaseClass()
{
    std::cout << "BaseClass dtor invoked" << std::endl;
}

void BaseClass::someMethod()
{
    std::cout << "Base class method" << std::endl;
}

DerivedClass::~DerivedClass()
{
    std::cout << "DerivedClass dtor invoked" << std::endl;
}

void DerivedClass::someMethod()
{
    std::cout << "Derived class method" << std::endl;
}

int main()
{
    BaseClass* b_ptr = new BaseClass;
    b_ptr->someMethod();
    delete b_ptr;
}

Quand j'ai construit le code ci-dessus (g ++ test.cpp -o test) et que j'ai vu le type de symboles générés comme suit,

nm - test de démangle

Je pouvais voir la sortie suivante.

==== following is partial output ====
08048816 T DerivedClass::someMethod()
08048922 T DerivedClass::~DerivedClass()
080489aa T DerivedClass::~DerivedClass()
08048a32 T DerivedClass::~DerivedClass()
08048842 T BaseClass::someMethod()
0804886e T BaseClass::~BaseClass()
080488f6 T BaseClass::~BaseClass()

Mes questions sont les suivantes.

1) Pourquoi plusieurs dtors ont-ils été générés (BaseClass - 2, DerivedClass - 3)?

2) Quelle est la différence entre ces dtors? Comment ces multiples détecteurs seront-ils utilisés de manière sélective?

J'ai maintenant le sentiment que pour atteindre une couverture de fonctions à 100% pour le projet C ++, nous aurions besoin de comprendre cela afin que je puisse invoquer tous ces dtors dans mes tests unitaires.

J'apprécierais beaucoup si quelqu'un pouvait me donner la réponse sur ce qui précède.

Smg
la source
5
+1 pour inclure un exemple de programme minimal et complet. ( sscce.org )
Robᵩ
2
Votre classe de base a-t-elle intentionnellement un destructeur non virtuel?
Kerrek SB
2
Une petite observation; vous avez péché et vous n'avez pas rendu votre destructeur BaseClass virtuel.
Lyke
Désolé pour mon échantillon incomplet. Oui, la BaseClass doit avoir un destructeur virtuel pour que ces objets de classe puissent être utilisés de manière polymorphe.
Smg
1
@Lyke: eh bien, si vous savez que vous n'allez pas supprimer un dérivé via un pointeur vers la base c'est OK, je m'assurais juste ... curieusement, si vous rendez les membres de base virtuels, vous obtenez même plus de destructeurs.
Kerrek SB

Réponses:

74

Tout d'abord, les objectifs de ces fonctions sont décrits dans l' ABI Itanium C ++ ; voir les définitions sous "destructeur d'objet de base", "destructeur d'objet complet" et "suppression du destructeur". La correspondance avec les noms mutilés est donnée en 5.1.4.

Fondamentalement:

  • D2 est le "destructeur d'objet de base". Il détruit l'objet lui-même, ainsi que les membres de données et les classes de base non virtuelles.
  • D1 est le "destructeur d'objets complet". Il détruit en outre les classes de base virtuelles.
  • D0 est le "destructeur d'objet de suppression". Il fait tout ce que fait le destructeur d'objets complet, en plus d'appeler operator deleteà libérer la mémoire.

Si vous n'avez pas de classes de base virtuelles, D2 et D1 sont identiques; GCC, à des niveaux d'optimisation suffisants, alias les symboles sur le même code pour les deux.

bdonlan
la source
Merci pour la réponse claire. Maintenant que je peux m'identifier, bien que je doive étudier davantage, car je ne suis pas très familier avec le genre d'héritage virtuel.
Smg
@Smg: en héritage virtuel, les classes héritées "virtuellement" sont sous la seule responsabilité de l'objet le plus dérivé. Autrement dit, si vous avez struct B: virtual Aet puis struct C: B, alors lors de la destruction d'un Bvous invoquez B::D1qui à son tour invoque A::D2et lors de la destruction d'un Cvous invoquez C::D1qui invoque B::D2et A::D2(notez comment B::D2n'invoque pas un destructeur). Ce qui est vraiment étonnant dans cette subdivision, c'est de pouvoir réellement gérer toutes les situations avec une simple hiérarchie linéaire de 3 destructeurs.
Matthieu M.
Hmm, je n'ai peut-être pas bien compris le point ... Je pensais que dans le premier cas (destruction de l'objet B), A :: D1 sera invoqué à la place de A :: D2. Et aussi dans le second cas (destruction de l'objet C), A :: D1 sera invoqué à la place de A :: D2. Ai-je tort?
Smg
A :: D1 n'est pas invoqué car A n'est pas ici la classe de niveau supérieur; la responsabilité de détruire les classes de base virtuelles de A (qui peuvent exister ou non) n'appartient pas à A, mais plutôt au D1 ou D0 de la classe de niveau supérieur.
bdonlan
37

Il existe généralement deux variantes du constructeur ( non-responsable / responsable ) et trois du destructeur ( suppression non-responsable / responsable / responsable ).

Les ctor et dtor non en charge sont utilisés lors de la gestion d'un objet d'une classe qui hérite d'une autre classe à l'aide du virtualmot - clé, lorsque l'objet n'est pas l'objet complet (donc l'objet actuel n'est "pas chargé" de la construction ou de la destruction l'objet de base virtuel). Ce ctor reçoit un pointeur vers l'objet de base virtuel et le stocke.

Le en charge sont pour tous les autres cas, par exemple , s'il n'y a pas d' héritage virtuel impliqué cteur et dtors; si la classe a un destructeur virtuel, le pointeur dtor de suppression en charge va dans le slot vtable, tandis qu'un scope qui connaît le type dynamique de l'objet (c'est-à-dire pour les objets avec une durée de stockage automatique ou statique) utilisera le dtor en charge (car cette mémoire ne doit pas être libérée).

Exemple de code:

struct foo {
    foo(int);
    virtual ~foo(void);
    int bar;
};

struct baz : virtual foo {
    baz(void);
    virtual ~baz(void);
};

struct quux : baz {
    quux(void);
    virtual ~quux(void);
};

foo::foo(int i) { bar = i; }
foo::~foo(void) { return; }

baz::baz(void) : foo(1) { return; }
baz::~baz(void) { return; }

quux::quux(void) : foo(2), baz() { return; }
quux::~quux(void) { return; }

baz b1;
std::auto_ptr<foo> b2(new baz);
quux q1;
std::auto_ptr<foo> q2(new quux);

Résultats:

  • L'entrée de dtor dans chacune des vtables pour foo, bazet quuxpoint respectif de suppression en charge dtor.
  • b1et b2sont construits par le baz() responsable , qui appelle le foo(1) responsable
  • q1et q2sont construits par le quux() responsable , qui tombe foo(2) en charge et baz() non en charge avec un pointeur vers l' fooobjet qu'il a construit précédemment
  • q2est détruit par ~auto_ptr() in-charge , qui appelle le dtor virtuel ~quux() en charge la suppression , qui appelle ~baz() non-responsable , ~foo() responsable et operator delete.
  • q1est détruit par le ~quux() responsable , qui appelle ~baz() non-responsable et ~foo() responsable
  • b2est détruit par le ~auto_ptr() responsable , qui appelle le dtor virtuel ~baz() en charge la suppression , qui appelle le ~foo() responsable etoperator delete
  • b1est détruit par le ~baz() responsable , qui appelle le ~foo() responsable

Toute personne dérivant de quuxutiliserait son ctor et dtor non-en charge et prendrait la responsabilité de créer l' fooobjet.

En principe, la variante sans charge n'est jamais nécessaire pour une classe qui n'a pas de bases virtuelles; dans ce cas, la variante en charge est alors parfois appelée unifiée , et / ou les symboles à la fois en charge et non en charge sont aliasés sur une seule implémentation.

Simon Richter
la source
Merci pour votre explication claire en conjonction avec un exemple assez facile à comprendre. Dans le cas où l'héritage virtuel est impliqué, c'est la responsabilité de la classe la plus dérivée de créer un objet de classe de base virtuel. Quant aux autres classes que la classe la plus dérivée, elles sont censées être interprétées par un constructeur non responsable afin de ne pas toucher la classe de base virtuelle.
Smg
Merci pour l'explication limpide. Je voulais obtenir des éclaircissements sur plus de chose, que se passe-t-il si nous n'utilisons pas auto_ptr et allouons à la place de la mémoire dans le constructeur et supprimons dans le destructeur. Dans ce cas, aurions-nous seulement deux destructeurs non-responsables / responsables de la suppression?
nonenone le
1
@bhavin, non, la configuration reste exactement la même. Le code généré pour un destructeur détruit toujours l'objet lui-même et tous les sous-objets, de sorte que vous obtenez le code de l' deleteexpression soit dans le cadre de votre propre destructeur, soit dans le cadre des appels du destructeur de sous-objet. L' deleteexpression est implémentée soit en tant qu'appel via la table virtuelle de l'objet s'il possède un destructeur virtuel (où nous trouvons la suppression en charge , soit en tant qu'appel direct au destructeur en charge de l'objet .
Simon Richter
Une deleteexpression n'appelle jamais la variante non chargée , qui n'est utilisée que par d'autres destructeurs lors de la destruction d'un objet qui utilise l'héritage virtuel.
Simon Richter