В типичной реализации деструктор обычно имеет две ветви: одну для уничтожения нединамического объекта, другую для уничтожения динамического объекта.Выбор конкретной ветви выполняется через скрытый логический параметр, передаваемый деструктору вызывающей стороной.Обычно он передается через регистр как 0 или 1.
Я бы предположил, что, поскольку в вашем случае уничтожение происходит для нединамического объекта, динамическая ветвь не берется.Попробуйте добавить объект new
-ed, а затем delete
-ed класса Foo
, и вторая ветвь также должна быть получена.
Причина, по которой это ветвление необходимо, коренится в спецификации C ++.язык.Когда некоторый класс определяет свой собственный 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
Обратите внимание на кодовые блоки, которые я пометил стрелками.Это именно то, о чем я говорил.Регистр al
служит этим скрытым параметром.Предполагается, что эта «псевдо-ветвь» либо вызывает, либо пропускает вызов на operator delete
в соответствии со значением al
.Однако в первой версии деструктора этот параметр жестко закодирован в теле как всегда 0
, а во второй он жестко закодирован как всегда 1
.
Класс B
также имеет две версиидеструктора, созданного для него.Таким образом, мы получаем 4 отличительных деструктора в скомпилированной программе: два деструктора для каждого класса.
Я могу предположить, что в начале компилятор внутренне мыслил в терминах одного «параметризованного» деструктора (который работает точно так же, какЯ описал выше перерыв).Затем было принято решение разделить параметризованный деструктор на две независимые непараметрические версии: одна для значения параметра с жестким кодом 0
(нединамический деструктор), а другая для значения параметра с жесткой кодировкой 1
(динамический деструктор).В неоптимизированном режиме он делает это буквально, присваивая фактическое значение параметра в теле функции и оставляя все ветвления полностью нетронутыми.Я думаю, это приемлемо в неоптимизированном коде.И это именно то, с чем вы имеете дело.
Другими словами, ответ на ваш вопрос: Невозможно заставить компилятор взять все ветви в этом случае. Нет возможности достичь 100% покрытия. Некоторые из этих веток «мертвы». Просто в этой версии GCC подход к генерации неоптимизированного кода довольно «ленив» и «бесполезен».
Может быть, есть способ предотвратить раскол в неоптимизированном режиме, я думаю. Я просто еще не нашел это. Или, вполне возможно, это не может быть сделано. В старых версиях GCC использовались истинно параметризованные деструкторы. Возможно, в этой версии GCC они решили перейти на подход с двумя деструкторами, и, делая это, они так быстро и «грязно» использовали существующий генератор кода, ожидая, что оптимизатор очистит ненужные ветви.
Когда вы компилируете с включенной оптимизацией, GCC не допустит такой роскоши, как бесполезное ветвление в конечном коде. Возможно, вам следует попытаться проанализировать оптимизированный код. Неоптимизированный код, сгенерированный GCC, имеет множество бессмысленных недоступных веток, подобных этой.