GNU GCC (g ++): Por que ele gera vários dtors?

89

Ambiente de desenvolvimento: GNU GCC (g ++) 4.1.2

Enquanto estou tentando investigar como aumentar a 'cobertura de código - particularmente a cobertura de função' em testes de unidade, descobri que parte da classe dtor parece ser gerada várias vezes. Alguns de vocês têm ideia do porquê, por favor?

Eu tentei e observei o que mencionei acima usando o código a seguir.

Em "test.h"

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

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

Em "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;
}

Quando eu construí o código acima (g ++ test.cpp -o test) e vejo que tipo de símbolos foram gerados da seguinte maneira,

nm - teste demangle

Eu pude ver a seguinte saída.

==== 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()

Minhas perguntas são as seguintes.

1) Por que vários dtors foram gerados (BaseClass - 2, DerivedClass - 3)?

2) Quais são as diferenças entre esses dtores? Como esses múltiplos dtors serão usados ​​seletivamente?

Agora tenho a sensação de que, para atingir 100% de cobertura de função para o projeto C ++, precisaríamos entender isso para que eu possa invocar todos aqueles dtors em meus testes de unidade.

Eu apreciaria muito se alguém pudesse me dar a resposta sobre o acima.

Smg
fonte
5
+1 por incluir um programa de amostra mínimo e completo. ( sscce.org )
Robᵩ
2
Sua classe base tem intencionalmente um destruidor não virtual?
Kerrek SB
2
Uma pequena observação; você pecou e não tornou seu destruidor BaseClass virtual.
Lyke
Desculpe pela minha amostra incompleta. Sim, a BaseClass deve ter destruidor virtual para que esses objetos de classe possam ser usados ​​polimorficamente.
Smg de
1
@Lyke: bem, se você sabe que não vai deletar um derivado por meio de um ponteiro para a base, tudo bem, eu estava apenas me certificando ... curiosamente, se você tornar os membros da base virtuais, você se vingará mais destruidores.
Kerrek SB

Respostas:

73

Primeiro, as finalidades dessas funções são descritas no Itanium C ++ ABI ; consulte as definições em "destruidor de objeto básico", "destruidor de objeto completo" e "excluindo destruidor". O mapeamento para nomes mutilados é fornecido em 5.1.4.

Basicamente:

  • D2 é o "destruidor de objeto básico". Ele destrói o próprio objeto, bem como membros de dados e classes base não virtuais.
  • D1 é o "destruidor de objeto completo". Além disso, destrói as classes básicas virtuais.
  • D0 é o "destruidor de objeto de exclusão". Ele faz tudo o que o destruidor de objeto completo faz, mais ele chama operator deletepara realmente liberar a memória.

Se você não tem classes base virtuais, D2 e ​​D1 são idênticos; O GCC irá, em níveis de otimização suficientes, criar um alias dos símbolos para o mesmo código para ambos.

bdonlan
fonte
Obrigado pela resposta clara. Agora que posso me identificar, embora precise estudar mais, pois não estou tão familiarizado com o tipo de herança virtual.
Smg
@Smg: na herança virtual, as classes herdadas "virtualmente" ficam sob a responsabilidade exclusiva do objeto mais derivado. Isto é, se você tiver struct B: virtual Ae então struct C: B, então ao destruir um Bvocê invoca B::D1que por sua vez invoca A::D2e ao destruir um Cvocê invoca C::D1que invoca B::D2e A::D2(observe como B::D2não invoca um destruidor). O que realmente é incrível nesta subdivisão é ser capaz de gerenciar todas as situações com uma hierarquia linear simples de 3 destruidores.
Matthieu M.
Hmm, posso não ter entendido o ponto claramente ... Eu pensei que no primeiro caso (destruindo o objeto B), A :: D1 será invocado em vez de A :: D2. E também no segundo caso (destruindo o objeto C), A :: D1 será invocado em vez de A :: D2. Estou errado?
Smg de
A :: D1 não é invocado porque A não é a classe de nível superior aqui; a responsabilidade pela destruição das classes base virtuais de A (que podem ou não existir) não pertence a A, mas sim a D1 ou D0 da classe de nível superior.
bdonlan
37

Normalmente, há duas variantes do construtor ( não responsável / responsável ) e três do destruidor ( exclusão não responsável / responsável / responsável ).

O ctor e dtor não-responsáveis são usados ​​ao manusear um objeto de uma classe que herda de outra classe usando a virtualpalavra - chave, quando o objeto não é o objeto completo (então o objeto atual "não está encarregado" de construir ou destruir o objeto de base virtual). Este ctor recebe um ponteiro para o objeto de base virtual e o armazena.

O encarregado ctor e dtors são para todos os outros casos, ou seja, se não houver nenhuma herança virtual envolvidos; se a classe tiver um destruidor virtual, o ponteiro responsável pela exclusão do dtor vai para o slot vtable, enquanto um escopo que conhece o tipo dinâmico do objeto (ou seja, para objetos com duração de armazenamento automático ou estático) usará o dtor responsável (porque essa memória não deve ser liberada).

Exemplo de código:

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);

Resultados:

  • A entrada dtor em cada um dos vtables para foo, baze quuxponto na respectiva exclusão em-carga dtor.
  • b1e b2são construídos pelo baz() responsável , que liga o foo(1) responsável
  • q1e q2são construídos pelo quux() responsável , que fica foo(2) responsável e baz() não responsável por um ponteiro para o fooobjeto que ele construiu anteriormente
  • q2é destruído pelo ~auto_ptr() responsável , que chama o dtor virtual ~quux() responsável pela exclusão , que chama o ~baz() não responsável , o ~foo() responsável e operator delete.
  • q1é destruído pelo ~quux() responsável , que liga para o ~baz() não-responsável e o ~foo() responsável
  • b2é destruído pelo ~auto_ptr() responsável , que chama o dtor virtual ~baz() responsável pela exclusão , que chama o ~foo() responsável eoperator delete
  • b1é destruído pelo ~baz() responsável , que liga o ~foo() responsável

Qualquer derivado de quuxusaria seu ctor e dtor não-responsáveis e assumiria a responsabilidade de criar o fooobjeto.

Em princípio, a variante sem carga nunca é necessária para uma classe que não possui bases virtuais; nesse caso, a variante responsável é então às vezes chamada de unificada e / ou os símbolos para responsável e não-responsável são aliasados ​​para uma única implementação.

Simon Richter
fonte
Obrigado por sua explicação clara em conjunto com um exemplo bastante fácil de entender. No caso de haver herança virtual envolvida, é responsabilidade das classes mais derivadas criar um objeto de classe base virtual. Quanto às outras classes além da classe mais derivada, elas devem ser interpretadas por um construtor não responsável, de forma que não afetem a classe base virtual.
Smg
Obrigado pela explicação cristalina. Eu queria obter mais esclarecimentos, e se não usarmos auto_ptr e, em vez disso, alocarmos memória no construtor e excluirmos no destruidor. Nesse caso, teríamos apenas dois destruidores não-responsáveis ​​/ excluindo-os?
nonenone
1
@bhavin, não, a configuração permanece exatamente a mesma. O código gerado para um destruidor sempre destrói o próprio objeto e quaisquer subobjetos, portanto, você obtém o código para a deleteexpressão como parte de seu próprio destruidor ou como parte das chamadas do destruidor de subobjeto. A deleteexpressão é implementada como uma chamada por meio da vtable do objeto se ele tiver um destruidor virtual (onde encontramos a exclusão responsável , ou como uma chamada direta ao destruidor responsável pelo objeto .
Simon Richter
Uma deleteexpressão nunca chama a variante não responsável , que é usada apenas por outros destruidores enquanto destrói um objeto que usa herança virtual.
Simon Richter