GNU GCC (g ++): ¿Por qué genera múltiples dtores?


90

Entorno de desarrollo: GNU GCC (g ++) 4.1.2

Mientras intento investigar cómo aumentar la 'cobertura de código, en particular la cobertura de funciones' en las pruebas unitarias, descubrí que parte de la clase dtor parece generarse varias veces. ¿Alguno de ustedes tiene alguna idea de por qué, por favor?

Intenté y observé lo que mencioné anteriormente usando el siguiente código.

En "test.h"

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

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

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

Cuando construí el código anterior (g ++ test.cpp -o test) y luego veo qué tipo de símbolos se han generado de la siguiente manera,

nm - prueba de descomposición

Pude ver el siguiente resultado.

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

Mis preguntas son las siguientes:

1) ¿Por qué se han generado varios dtors (BaseClass - 2, DerivedClass - 3)?

2) ¿Cuáles son las diferencias entre estos dtores? ¿Cómo se utilizarán selectivamente esos múltiples dtores?

Ahora tengo la sensación de que para lograr una cobertura de funciones del 100% para el proyecto C ++, deberíamos entender esto para poder invocar todos esos dtors en mis pruebas unitarias.

Le agradecería mucho si alguien me pudiera dar la respuesta a lo anterior.


5
+1 por incluir un programa de muestra mínimo y completo. ( sscce.org )
Robᵩ

2
¿Su clase base tiene intencionalmente un destructor no virtual?
Kerrek SB

2
Una pequeña observación; ha pecado y no ha hecho virtual su destructor BaseClass.
Lyke

Perdón por mi muestra incompleta. Sí, BaseClass debe tener un destructor virtual para que estos objetos de clase se puedan usar polimórficamente.
Smg

1
@Lyke: así, si usted sabe que usted no va a eliminar una deriva a través de una base de puntero-a-eso está bien, yo estaba asegurando ... curiosamente, si hacen los miembros de base virtual, se obtiene incluso más destructores.
Kerrek SB

Respuestas:


74

Primero, los propósitos de estas funciones se describen en la ABI de Itanium C ++ ; consulte las definiciones en "destructor de objeto base", "destructor de objeto completo" y "destructor de eliminación". El mapeo a nombres mutilados se da en 5.1.4.

Básicamente:

  • D2 es el "destructor de objetos base". Destruye el objeto en sí, así como los miembros de datos y las clases base no virtuales.
  • D1 es el "destructor de objetos completo". Además, destruye las clases base virtuales.
  • D0 es el "destructor de objeto de eliminación". Hace todo lo que hace el destructor de objetos completo, además de llamar operator deletepara liberar la memoria.

Si no tiene clases de base virtuales, D2 y D1 son idénticas; GCC, en niveles de optimización suficientes, alias los símbolos con el mismo código para ambos.


Gracias por la clara respuesta. Ahora que puedo relacionarme, aunque necesito estudiar más porque no estoy tan familiarizado con el tipo de cosas de herencia virtual.
Smg

@Smg: en la herencia virtual, las clases heredadas "virtualmente" están bajo la responsabilidad exclusiva del objeto más derivado. Es decir, si tiene struct B: virtual Ay luego struct C: B, al destruir un B, invoca el B::D1que a su vez invoca A::D2y al destruir un C, invoca el C::D1que invoca B::D2y A::D2(observe cómo B::D2no invoca un destructor). Lo realmente sorprendente de esta subdivisión es poder gestionar todas las situaciones con una jerarquía lineal simple de 3 destructores.
Matthieu M.

Hmm, puede que no haya entendido el punto claramente ... Pensé que en el primer caso (destruyendo el objeto B), se invocará A :: D1 en lugar de A :: D2. Y también en el segundo caso (destruyendo el objeto C), se invocará A :: D1 en lugar de A :: D2. ¿Me equivoco?
Smg

A :: D1 no se invoca porque A no es la clase de nivel superior aquí; la responsabilidad de destruir las clases base virtuales de A (que pueden existir o no) no pertenece a A, sino a la D1 o D0 de la clase de nivel superior.
bdonlan

37

Por lo general, hay dos variantes del constructor ( no a cargo / a cargo ) y tres del destructor ( no a cargo / a cargo / borrado a cargo ).

El ctor y dtor no a cargo se usan cuando se maneja un objeto de una clase que hereda de otra clase usando la virtualpalabra clave, cuando el objeto no es el objeto completo (por lo que el objeto actual "no está a cargo" de construir o destruir el objeto base virtual). Este ctor recibe un puntero al objeto base virtual y lo almacena.

El ctor y los dtors a cargo son para todos los demás casos, es decir, si no hay una herencia virtual involucrada; si la clase tiene un destructor virtual, el puntero dtor de eliminación encargado va a la ranura vtable, mientras que un osciloscopio que conoce el tipo dinámico del objeto (es decir, para objetos con duración de almacenamiento automático o estático) utilizará el dtor encargado (porque esta memoria no debe liberarse).

Ejemplo 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:

  • La entrada dtor en cada una de las vtables para foo , bazy quuxpunto en el respectivo a cargo de borrado DTOR.
  • b1y b2están construidos porbaz() encargado , que llama foo(1) encargado
  • q1y q2están construidos por quux() el encargado , que caefoo(2) a cargo y baz() no a cargo con un puntero al fooobjeto que construyó anteriormente
  • q2es destruido por ~auto_ptr() in-charge , que llama al dtor virtual ~quux() a cargo borrado , que llama ~baz() no-a-charge , ~foo() in-charge y operator delete.
  • q1es destruido por ~quux() el encargado , que llama al ~baz() no responsable y ~foo() al responsable
  • b2es destruido por ~auto_ptr() el encargado , que llama al dtor virtual ~baz() a cargo borrar , que llama ~foo() al encargado yoperator delete
  • b1 es destruido por ~baz() el encargado , que llama ~foo() al encargado

Cualquiera que se derive de quux usaría su no a cargo ctor y dtor y asumir la responsabilidad de crear el fooobjeto.

En principio, la variante sin cargo nunca es necesaria para una clase que no tiene bases virtuales; en ese caso, la variante a cargo a veces se denomina unificada , y / o los símbolos tanto para el responsable como para el no responsable se alias en una sola implementación.


Gracias por su clara explicación junto con un ejemplo bastante fácil de entender. En caso de que se trate de una herencia virtual, la responsabilidad de la clase más derivada es crear un objeto de clase base virtual. En cuanto a las otras clases además de la clase más derivada, se supone que deben ser construidas por un constructor no responsable para que no toquen la clase base virtual.
Smg

Gracias por una explicación muy clara. Quería aclarar más cosas, ¿qué pasa si no usamos auto_ptr y en su lugar asignamos memoria en el constructor y eliminamos en el destructor? En ese caso, ¿tendríamos solo dos destructores no a cargo / a cargo de eliminar?
nonenone

1
@bhavin, no, la configuración sigue siendo exactamente la misma. El código generado para un destructor siempre destruye el objeto en sí y cualquier subobjeto, por lo que obtiene el código de la deleteexpresión como parte de su propio destructor o como parte de las llamadas al destructor del subobjeto. La deleteexpresión se implementa como una llamada a través de vtable del objeto si tiene un destructor virtual (donde encontramos la eliminación a cargo , o como una llamada directa al destructor a cargo del objeto .
Simon Richter

Una deleteexpresión nunca llama a la variante no responsable , que solo la usan otros destructores mientras destruyen un objeto que usa herencia virtual.
Simon Richter
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.