101 votes

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

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é que certains dtor de classe semblent être générés plusieurs fois. L'un d'entre vous a-t-il une idée de la raison, 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;
}

Lorsque j'ai construit le code ci-dessus (g++ test.cpp -o test) et que je vois ensuite quels types de symboles ont été générés comme suit,

nm --demangle test

J'ai pu 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) Quelles sont les différences entre ces dtors ? Comment ces dtors multiples seront-ils utilisés de manière sélective ?

J'ai maintenant le sentiment que pour atteindre une couverture fonctionnelle de 100 % pour un projet C++, il faudrait que nous comprenions cela pour que je puisse invoquer tous ces dtors dans mes tests unitaires.

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

6 votes

+1 pour avoir inclus un programme d'exemple minimal et complet. ( sscce.org )

2 votes

Votre classe de base a-t-elle intentionnellement un destructeur non virtuel ?

2 votes

Une petite observation ; vous avez péché, et n'avez pas rendu virtuel le destructeur de votre BaseClass.

86voto

bdonlan Points 90068

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

En gros :

  • D2 est le "destructeur de l'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'objet complet". Il détruit en plus les classes de base virtuelles.
  • D0 est le "destructeur d'objet de suppression". Il fait tout ce que fait le destructeur d'objet complet, plus un appel à operator delete pour libérer réellement la mémoire.

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

0 votes

Merci pour cette réponse claire. Je peux maintenant m'y retrouver, bien que je doive étudier davantage car je ne suis pas très familier avec les questions d'héritage virtuel.

0 votes

@Smg : dans l'héritage virtuel, les classes héritées "virtuellement" sont sous la seule responsabilité de l'objet le plus dérivé. C'est-à-dire que si vous avez struct B: virtual A et ensuite struct C: B puis, lors de la destruction d'un B vous invoquez B::D1 qui, à leur tour, invoquent A::D2 et lors de la destruction d'un C vous invoquez C::D1 qui invoquent B::D2 y A::D2 (notez comment B::D2 n'invoque pas le destructeur A). Ce qui est vraiment étonnant dans cette subdivision, c'est de pouvoir gérer toutes les situations avec une simple hiérarchie linéaire de 3 destructrices.

0 votes

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é au lieu de A::D2. Et aussi dans le deuxième cas (destruction de l'objet C), A::D1 sera invoqué au lieu de A::D2. Est-ce que je me trompe ?

37voto

Simon Richter Points 11471

Il existe généralement deux variantes du constructeur ( non responsable / responsable ) et trois du destructeur ( non responsable / responsable / responsable de l'effacement ).

El non responsable ctor et dtor sont utilisés lors de la manipulation d'un objet d'une classe qui hérite d'une autre classe à l'aide de l'attribut virtual lorsque l'objet n'est pas l'objet complet (l'objet courant n'est donc "pas en charge" de la construction ou de la destruction de l'objet de base virtuel). Ce ctor reçoit un pointeur vers l'objet de base virtuel et le stocke.

El responsable ctor et dtors le sont pour tous les autres cas, c'est-à-dire s'il n'y a pas d'héritage virtuel impliqué ; si la classe a un destructeur virtuel, la fonction responsable de l'effacement va dans le slot vtable, tandis qu'une portée 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 pointeur de dtor responsable dtor (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 dtor dans chacune des vtables pour foo , baz y quux aux points respectifs de responsable de l'effacement dtor.
  • b1 y b2 sont construites par baz() responsable qui appelle foo(1) responsable
  • q1 y q2 sont construites par quux() responsable qui tombe foo(2) responsable y baz() non responsable avec un pointeur sur le foo l'objet qu'il a construit précédemment
  • q2 est détruite par ~auto_ptr() responsable qui appelle le dtor virtuel ~quux() responsable de l'effacement qui appelle ~baz() non responsable , ~foo() responsable y operator delete .
  • q1 est détruite par ~quux() responsable qui appelle ~baz() non responsable y ~foo() responsable
  • b2 est détruite par ~auto_ptr() responsable qui appelle le dtor virtuel ~baz() responsable de l'effacement qui appelle ~foo() responsable y operator delete
  • b1 est détruite par ~baz() responsable qui appelle ~foo() responsable

Toute personne dérivant de quux utiliserait son non responsable ctor et dtor et assumer la responsabilité de la création de la foo objet.

En principe, le non responsable n'est jamais nécessaire pour une classe qui ne possède pas de bases virtuelles ; dans ce cas, la variante responsable La variante est alors parfois appelée unifié et/ou les symboles pour les deux responsable y non responsable sont aliasés à une seule implémentation.

0 votes

Merci pour votre explication claire accompagnée d'un exemple très facile à comprendre. Dans le cas où l'héritage virtuel est impliqué, il est de la responsabilité de la classe la plus dérivée de créer l'objet virtuel de la classe de base. Quant aux autres classes que la classe la plus dérivée, elles sont censées être construites par un constructeur non responsable afin de ne pas toucher à la classe de base virtuelle.

0 votes

Merci pour cette explication claire et nette. Je voulais obtenir des éclaircissements sur une autre chose, que se passe-t-il si nous n'utilisons pas auto_ptr et si nous allouons la mémoire dans le constructeur et la supprimons dans le destructeur. Dans ce cas, n'aurions-nous que deux destructeurs, non chargés et chargés de la suppression ?

1 votes

@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, donc vous obtenez le code pour le destructeur de l'objet. delete soit dans le cadre de votre propre destructeur, soit dans le cadre des appels au destructeur du sous-objet. L'adresse delete est implémentée soit comme un appel à travers la vtable de l'objet s'il a un destructeur virtuel (où nous trouvons l'expression responsable de l'effacement ou en tant qu'appel direct à l'objet responsable destructeur.

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