In a nutshell: g++ creates two destructors for a class
- One for destructing of the objects.
- One for destructing of the objects, which were allocated on the heap.
In some scenarios both of them a kept in the object file and in some only the used. In your 75%-coverage-example you use only the first destructor, but both have to be kept in the object file.
The link in the answer of @MSalters shows the direction, but it is mostly about multiple constructor/destructor symbols emitted by the g++.
At least to me, it doesn't become directly obvious from this linked answer, what is going on, thus I would like to elaborate .
First case (100% coverage):
Let's start with a slightly different definition of the Animal
-class, one without the virtual
destructor:
class Animal
{
public:
Animal(){}
~Animal(){}
};
int main(){Animal animal;}
For this class-definition lcov shows 100% code coverage.
Let's take a look at the symbols in the object file (I built it without gcov for the sake of simplicity):
nm main.o
0000000000000000 T main
U __stack_chk_fail
0000000000000000 W _ZN6AnimalC1Ev
0000000000000000 W _ZN6AnimalC2Ev
0000000000000000 n _ZN6AnimalC5Ev
0000000000000000 W _ZN6AnimalD1Ev
0000000000000000 W _ZN6AnimalD2Ev
0000000000000000 n _ZN6AnimalD5Ev
Compiler keeps only those inline functions, which are needed in main
(functions implemented in a class definition are treated as inline functions, for example there are no copy-constructor or assignment-operator, which are automatically defined by the compiler). I'm not sure what AnimalX5Ev
are, but for this class there is no difference for AnimalXC1Ev
(complete object constructor) and AnimalXC2Ev
(base object constructor) - they have even the same address. As explained in the linked answer, it is some quirks of gcc (but clang has it as well) and byproduct of polymorphism support.
Second case (75% coverage):
Let's make the destructor virtual as in our original example, and look at the symbol in the resulting object file:
nm main.o
0000000000000000 T main
...
0000000000000000 W _ZN6AnimalD0Ev <----------- NEW
...
0000000000000000 V _ZTV6Animal <----------- NEW
And we see some new symbols: _ZTV6Animal
is the well known vtable, and _ZN6AnimalD0Ev
- the so called deleting destructor (read on to see why it is needed). However, in main
once again only _ZN6AnimalD1Ev
is used, because nothing changed compared to the first case (compile with g++ -S main.cpp -o main.s
to see it).
But why on earth is _ZN6AnimalD0Ev
kept in the object file, if it is not used? Because it is used in the virtual table _ZTV6Animal
(see the assembly main.s
):
_ZTV6Animal:
.quad 0
.quad _ZTI6Animal
.quad _ZN6AnimalD1Ev
.quad _ZN6AnimalD0Ev <---- HERE is the address of the function!
.weak _ZTI6Animal
But why is this vtable needed? Because every object of a class has a reference to the vtable of the class as soon as there is a virtual method in the class, as can be see in the constructor (still main.s):
ZN6AnimalC2Ev:
...
// in register %rdi is the address of the newly created object
movl $_ZTV6Animal+16, (%rdi) ;write the address of the vtable (why +16?) to the address pointed to by %rdi.
...
I must confess, I simplified the assembly a little, but it is easy to see, that the memory layout of a Animal
-object starts with the address of the virtual table.
This dealocating destructor _ZN6AnimalD0Ev
is the function for which the coverage is missing - because it is not used in your program.
Third case (100% coverage again):
What changes, if we use new
+delete
? First we have to know, that destructing an object on the heap is a little bit different than calling a destructor for the object on the stack, because we need to:
- Destroy the object (it is the same as on the stack i.e.
_ZN6AnimalD1Ev
)
- Release/free memory which was occupied by the object on the heap.
These two steps are bundled together in the dealocating destructor _ZN6AnimalD0Ev
, as once again can be seen in the assembly:
_ZN6AnimalD0Ev:
call _ZN6AnimalD1Ev ; <---- call "Stack"-destructor
....
call _ZdlPv ; free heap memory
....
Now, in main
we have to delete the object from the heap, thus D0
-destructor-version must be called, which calls the D1
-destructor-version in its turn - which means all functions are used - 100% coverage again.
The last piece of the puzzle, why D0
-destructor is part of the virtual table? If animal
were a Cat
, how would main
know which dealocating destructor (the one of Cat
and not of Animal
) to call? By looking at the virtual table of the object pointed to by animal
and for this the D0
-destructor is included in the vtable.
However, this all are implementation details of g++, I don't think there is much in the standard enforcing it to be done this way. Nevertheless, clang++ does exactly the same, have to check for MSVS and intel though.
PS: Great article about deleting destructors.