Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function coverage is lesser even with 100% code coverage when objects created in the stack

Tags:

c++

lcov

gcov

I'm analyzing my code with gcov. It says my code is 2 functions lesser when objects are created in the stack. But when I do new-delete 100% function coverage is achieved.

Code:

class Animal
{
public:
    Animal()
    {
    }
    virtual ~Animal()
    {
    }
};

int main()
{
    Animal animal;
}

Commands I executed for generate the gcov report.

rm -rf Main.g* out.txt a.out coverage;
g++ -fprofile-arcs -ftest-coverage -lgcov -coverage Main.cpp;
./a.out;
lcov --capture --directory . --output-file out.txt;
genhtml out.txt --output-directory coverage;

Generated htmls shows my function coverage as 3/4 - 75%.

But once I change my stack object to a heap,

Code:

class Animal
{
public:
    Animal()
    {
    }
    virtual ~Animal()
    {
    }
};

int main()
{
    auto animal = new Animal;
    delete animal;
}

The my function coverage is 100%.

What are the hidden functions called only when calling "new" and "delete"?

like image 947
Sujith Gunawardhane Avatar asked Sep 27 '17 12:09

Sujith Gunawardhane


2 Answers

In a nutshell: g++ creates two destructors for a class

  1. One for destructing of the objects.
  2. 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:

  1. Destroy the object (it is the same as on the stack i.e. _ZN6AnimalD1Ev)
  2. 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 mainknow 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.

like image 105
ead Avatar answered Sep 21 '22 03:09

ead


They're the allocating constructor and deallocating destructor.

This is an implementation detail of g++.

like image 45
MSalters Avatar answered Sep 22 '22 03:09

MSalters