Подтвердить что ты не робот

Какая ветвь в деструкторе сообщается gcov?

Когда я использую gcov для измерения тестового покрытия кода С++, он сообщает ветки в деструкторах.

struct Foo
{
    virtual ~Foo()
    {
    }
};

int main (int argc, char* argv[])
{
    Foo f;
}

Когда я запускаю gcov с включенными вероятностями ветвления (-b), я получаю следующий вывод.

$ gcov /home/epronk/src/lcov-1.9/example/example.gcda -o /home/epronk/src/lcov-1.9/example -b
File 'example.cpp'
Lines executed:100.00% of 6
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Calls executed:40.00% of 5
example.cpp:creating 'example.cpp.gcov'

Часть, которая беспокоит меня, - это "Взято хотя бы один раз: 50,00% от 2".

Сгенерированный файл .gcov дает более подробную информацию.

$ cat example.cpp.gcov | c++filt
        -:    0:Source:example.cpp
        -:    0:Graph:/home/epronk/src/lcov-1.9/example/example.gcno
        -:    0:Data:/home/epronk/src/lcov-1.9/example/example.gcda
        -:    0:Runs:1
        -:    0:Programs:1
        -:    1:struct Foo
function Foo::Foo() called 1 returned 100% blocks executed 100%
        1:    2:{
function Foo::~Foo() called 1 returned 100% blocks executed 75%
function Foo::~Foo() called 0 returned 0% blocks executed 0%
        1:    3:    virtual ~Foo()
        1:    4:    {
        1:    5:    }
branch  0 taken 0% (fallthrough)
branch  1 taken 100%
call    2 never executed
call    3 never executed
call    4 never executed
        -:    6:};
        -:    7:
function main called 1 returned 100% blocks executed 100%
        1:    8:int main (int argc, char* argv[])
        -:    9:{
        1:   10:    Foo f;
call    0 returned 100%
call    1 returned 100%
        -:   11:}

Обратите внимание на строку "ответвление 0 принимается 0% (провал)".

Что вызывает эту ветку и что мне нужно сделать в коде, чтобы получить здесь 100%?

  • g++ (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
  • gcov (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
4b9b3361

Ответ 1

В типичной реализации деструктор обычно имеет две ветки: одну для уничтожения нединамических объектов, другую для уничтожения динамических объектов. Выбор конкретной ветки выполняется через скрытый логический параметр, переданный вызывающим абонентом деструктору. Обычно он передается через регистр как 0 или 1.

Я бы предположил, что, поскольку в вашем случае уничтожение для нединамического объекта, динамическая ветвь не берется. Попробуйте добавить объект new -ed, а затем delete -ed класса Foo, а вторая ветвь также должна быть взята.

Причина, по которой это ветвление необходимо, коренится в спецификации языка С++. Когда какой-то класс определяет свой собственный operator delete, выбор конкретного operator delete для вызова выполняется так, как если бы он искался изнутри деструктора класса. Конечным результатом этого является то, что для классов с виртуальным деструктором operator delete ведет себя так, как если бы это была виртуальная функция (несмотря на формальное статическое членство класса).

Многие компиляторы реализуют это поведение буквально: правильный operator delete вызывается непосредственно изнутри реализации деструктора. Конечно, operator delete следует вызывать только при уничтожении динамически распределенных объектов (не для локальных или статических объектов). Для этого вызов operator delete помещается в ветвь, контролируемую скрытым параметром, упомянутым выше.

В вашем примере все выглядит довольно тривиально. Я ожидаю, что оптимизатор удалит все ненужные ветвления. Однако кажется, что каким-то образом ему удалось выдержать оптимизацию.


Вот несколько дополнительных исследований. Рассмотрим этот код

#include <stdio.h>

struct A {
  void operator delete(void *) { scanf("11"); }
  virtual ~A() { printf("22"); }
};

struct B : A {
  void operator delete(void *) { scanf("33"); }
  virtual ~B() { printf("44"); }
};

int main() {
  A *a = new B;
  delete a;
} 

Вот как выглядит код деструктора A, когда компилятор с GCC 4.3.4 в настройках оптимизации по умолчанию

__ZN1AD2Ev:                      ; destructor A::~A  
LFB8:
        pushl   %ebp
LCFI8:
        movl    %esp, %ebp
LCFI9:
        subl    $8, %esp
LCFI10:
        movl    8(%ebp), %eax
        movl    $__ZTV1A+8, (%eax)
        movl    $LC1, (%esp)     ; LC1 is "22"
        call    _printf
        movl    $0, %eax         ; <------ Note this
        testb   %al, %al         ; <------ 
        je      L10              ; <------ 
        movl    8(%ebp), %eax    ; <------ 
        movl    %eax, (%esp)     ; <------ 
        call    __ZN1AdlEPv      ; <------ calling `A::operator delete`
L10:
        leave
        ret

(Деструктор B немного сложнее, поэтому я использую A здесь в качестве примера. Но что касается рассматриваемого ветвления, деструктор B делает это в том же путь).

Однако сразу после этого деструктора сгенерированный код содержит другую версию деструктора для того же класса A, который выглядит точно таким же, за исключением того, что инструкция movl $0, %eax заменяется на инструкцию movl $1, %eax.

__ZN1AD0Ev:                      ; another destructor A::~A       
LFB10:
        pushl   %ebp
LCFI13:
        movl    %esp, %ebp
LCFI14:
        subl    $8, %esp
LCFI15:
        movl    8(%ebp), %eax
        movl    $__ZTV1A+8, (%eax)
        movl    $LC1, (%esp)     ; LC1 is "22"
        call    _printf
        movl    $1, %eax         ; <------ See the difference?
        testb   %al, %al         ; <------
        je      L14              ; <------
        movl    8(%ebp), %eax    ; <------
        movl    %eax, (%esp)     ; <------
        call    __ZN1AdlEPv      ; <------ calling `A::operator delete`
L14:
        leave
        ret

Обратите внимание на блоки кода I, помеченные стрелками. Это именно то, о чем я говорил. Регистр al служит в качестве скрытого параметра. Эта "псевдо-ветвь" должна либо вызывать, либо пропускать вызов operator delete в соответствии со значением al. Однако в первой версии деструктора этот параметр жестко закодирован в тело, как всегда, 0, а во втором - жестко запрограммирован как всегда 1.

Класс B также имеет две версии созданного для него деструктора. Таким образом, мы получаем 4 отличительных деструктора в скомпилированной программе: два деструктора для каждого класса.

Я могу догадаться, что вначале компилятор внутренне мыслил в терминах одного "параметризованного" деструктора (который работает точно так же, как я описал выше разрыва). И затем он решил разделить параметризованный деструктор на две независимые непараметризированные версии: один для жестко заданного значения параметра 0 (нединамический деструктор), а другой для жестко заданного значения параметра 1 (динамический деструктор). В не оптимизированном режиме он делает это буквально, назначая фактическое значение параметра внутри тела функции и оставляя все ветвления полностью неповрежденными. Полагаю, это приемлемо для неоптимизированного кода. И это именно то, с чем вы имеете дело.

Другими словами, ответ на ваш вопрос: Невозможно заставить компилятор взять все ветки в этом случае. Нет возможности достичь 100% -ного охвата. Некоторые из этих ветвей "мертвы". Просто подход к генерации неоптимизированного кода довольно "ленив" и "свободен" в этой версии GCC.

Может быть, есть способ предотвратить разделение в неоптимизированном режиме, я думаю. Я еще не нашел его. Или, вполне возможно, это невозможно. В старых версиях GCC использовались истинные параметризованные деструкторы. Возможно, в этой версии GCC они решили переключиться на подход с двумя деструкторами, и при этом они "повторно использовали" существующий генератор кода таким быстрым и грязным способом, ожидая, что оптимизатор очистит бесполезные ветки.

При компиляции с включенной оптимизацией GCC не позволит себе такой роскоши, как бесполезное разветвление в конечном коде. Вероятно, вы должны попытаться проанализировать оптимизированный код. Неоптимизированный код, созданный GCC, содержит много бессмысленных недоступных ветвей, таких как этот.

Ответ 2

В деструкторе GCC генерирует условный переход для условия, которое никогда не может быть истинным (% al не равно нулю, поскольку ему просто присвоено значение 1):

[...]
  29:   b8 01 00 00 00          mov    $0x1,%eax
  2e:   84 c0                   test   %al,%al
  30:   74 30                   je     62 <_ZN3FooD0Ev+0x62>
[...]