Ответ 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, содержит много бессмысленных недоступных ветвей, таких как этот.