100

Developing environment: GNU GCC (g++) 4.1.2

While I'm trying to investigate how to increase 'code coverage - particularly function coverage' in unit testing, I've found that some of class dtor seems to be generated multiple times. Does some of you have any idea on why, please?

I tried and observed what I mentioned the above by using the following code.

In "test.h"

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

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

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

When I built the above code (g++ test.cpp -o test) and then see what kind of symbols have been generated as follows,

nm --demangle test

I could see the following output.

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

My questions are as follows.

1) Why multiple dtors have been generated (BaseClass - 2, DerivedClass - 3)?

2) What are the difference among these dtors? How those multiple dtors will be selectively used?

I now have a feeling that in order to achieve 100% function coverage for C++ project, we would need to understand this so that I can invoke all those dtors in my unit tests.

I would greately appreciate if someone could give me the reply on the above.

razlebe
  • 7,134
  • 6
  • 42
  • 57
Smg
  • 1,133
  • 1
  • 8
  • 9
  • 6
    +1 for including a minimal, complete sample program. (http://sscce.org) – Robᵩ Jul 07 '11 at 16:31
  • 2
    Does your base class intentionally have a non-virtual destructor? – Kerrek SB Jul 07 '11 at 16:39
  • 2
    A small observation; you have sinned, and not made your BaseClass destructor virtual. – Lyke Jul 07 '11 at 16:41
  • Sorry for my incomplete sample. Yes, the BaseClass should have virtual destructor so these class objects can be used polymorphically. – Smg Jul 07 '11 at 16:44
  • 1
    @Lyke: well, if you know that you're not going to delete a derived through a pointer-to-base that's OK, I was just making sure... funnily, if you do make the base members virtual, you get *even more* destructors. – Kerrek SB Jul 07 '11 at 16:44
  • When I changed BaseClass destructor as virtual, I observed BaseClass now have 3 destructors. Could someone let me know why? – Smg Jul 07 '11 at 16:50
  • sometime ago I tried to break into ctor / dtor in gdb and couldn't. So on gdb site, they pointed out that gcc generates different versions of object code for the same source-code, to enable linking with different C++ ABI. Not sure about the details from C++ Standard; but here's an Internet link http://www.cygwin.com/ml/gdb/2004-07/msg00162.html. – Abhay Jul 07 '11 at 17:06
  • I asked this in a different form a bit ago, and got two really excellent answers before anybody noticed it was a duplicate. Pasting a link here so people can find the other answers: https://stackoverflow.com/questions/44558119/why-do-i-have-two-destructor-implementations-in-my-assembly-output – Omnifarious Jun 17 '17 at 03:44

2 Answers2

84

First, the purposes of these functions are described in the Itanium C++ ABI; see definitions under "base object destructor", "complete object destructor", and "deleting destructor". The mapping to mangled names is given in 5.1.4.

Basically:

  • D2 is the "base object destructor". It destroys the object itself, as well as data members and non-virtual base classes.
  • D1 is the "complete object destructor". It additionally destroys virtual base classes.
  • D0 is the "deleting object destructor". It does everything the complete object destructor does, plus it calls operator delete to actually free the memory.

If you have no virtual base classes, D2 and D1 are identical; GCC will, on sufficient optimization levels, actually alias the symbols to the same code for both.

Paul Floyd
  • 5,530
  • 5
  • 29
  • 43
bdonlan
  • 224,562
  • 31
  • 268
  • 324
  • Thank you for the clear answer. Now that I can relate to, although I need to study more as I'm not so familiar with virtual inheritance kind of stuffs. – Smg Jul 07 '11 at 17:24
  • @Smg: in virtual inheritance, the "virtually" inherited classes are under the sole responsability of the most-derived object. That is, if you have `struct B: virtual A` and then `struct C: B`, then when destroying a `B` you invoke `B::D1` which in turns invoke `A::D2` and when destroying a `C` you invoke `C::D1` which invoke `B::D2` and `A::D2` (note how `B::D2` does not invoke A destructor). What really is amazing in this subdivision is to actually be able to manage all the situations with a simple linear hierarchy of *3* destructors. – Matthieu M. Jul 07 '11 at 18:24
  • Hmm, I may not have understood the point clearly... I thought that in the first case (destructing B object), A::D1 will be invoked instead of A::D2. And also in second case (destructing C object), A::D1 will be invoked instead of A::D2. Am I wrong? – Smg Jul 08 '11 at 02:41
  • A::D1 is not invoked because A is not the top-level class here; the responsibility for destroying virtual base classes of A (which may or may not exist) does not belong to A, but rather to the D1 or D0 of the top-level class. – bdonlan Jul 08 '11 at 02:50
37

There are usually two variants of the constructor (not-in-charge / in-charge) and three of the destructor (not-in-charge / in-charge / in-charge deleting).

The not-in-charge ctor and dtor are used when handling an object of a class that inherits from another class using the virtual keyword, when the object is not the complete object (so the current object is "not in charge" of constructing or destructing the virtual base object). This ctor receives a pointer to the virtual base object and stores it.

The in-charge ctor and dtors are for all the other cases, i.e. if there is no virtual inheritance involved; if the class has a virtual destructor, the in-charge deleting dtor pointer goes into the vtable slot, while a scope that knows the dynamic type of the object (i.e. for objects with automatic or static storage duration) will use the in-charge dtor (because this memory should not be freed).

Code example:

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

Results:

  • The dtor entry in each of the vtables for foo, baz and quux point at the respective in-charge deleting dtor.
  • b1 and b2 are constructed by baz() in-charge, which calls foo(1) in-charge
  • q1 and q2 are constructed by quux() in-charge, which falls foo(2) in-charge and baz() not-in-charge with a pointer to the foo object it constructed earlier
  • q2 is destructed by ~auto_ptr() in-charge, which calls the virtual dtor ~quux() in-charge deleting, which calls ~baz() not-in-charge, ~foo() in-charge and operator delete.
  • q1 is destructed by ~quux() in-charge, which calls ~baz() not-in-charge and ~foo() in-charge
  • b2 is destructed by ~auto_ptr() in-charge, which calls the virtual dtor ~baz() in-charge deleting, which calls ~foo() in-charge and operator delete
  • b1 is destructed by ~baz() in-charge, which calls ~foo() in-charge

Anyone deriving from quux would use its not-in-charge ctor and dtor and take on the responsibility of creating the foo object.

In principle, the not-in-charge variant is never needed for a class that has no virtual bases; in that case, the in-charge variant is then sometimes called unified, and/or the symbols for both in-charge and not-in-charge are aliased to a single implementation.

Simon Richter
  • 28,572
  • 1
  • 42
  • 64
  • Thank you for your clear explanation in conjunction with quite easy-to-understand example. In case that virtual inheritance is involved, it is the most derived class'es responsibility to create virtual base class object. As for the other classes than the most derived class, they are supposed to be construed by not-in-charge constructor so they don't touch the virtual base class. – Smg Jul 08 '11 at 01:18
  • Thanks for crystal clear explanation. I wanted to get clarification on more thing, what if we do not use auto_ptr and instead allocate memory in constructor and delete in destructor. In that case would we have only two destructors not-in-charge / in-charge deleting? – nonenone Jan 14 '20 at 20:42
  • 1
    @bhavin, no, the setup stays exactly the same. The generated code for a destructor always destroys the object itself and any sub-objects, so you get the code for the `delete` expression either as part of your own destructor, or as part of the sub-object destructor calls. The `delete` expression is implemented either as a call through the object's vtable if it has a virtual destructor (where we find *in-charge deleting*, or as a direct call to the object's *in-charge* destructor. – Simon Richter Jan 18 '20 at 10:49
  • A `delete` expression never calls the *not-in-charge* variant, that's only used by other destructors while destroying an object that uses virtual inheritance. – Simon Richter Jan 18 '20 at 10:49