В вашем вопросе есть две разные части волшебства .Первый - как компилятор вызывает последний переопределитель для деструктора, а второй - как он затем вызывает все остальные деструкторы по порядку.
Отказ от ответственности: Стандарт не требует какого-либо конкретного способа выполненияэти операции, он только предписывает поведение операций на более высоком уровне.Это детали реализации, которые являются общими для различных реализаций, но не предписаны стандартом.
Как компилятор отправляет окончательную переопределение?
Первый ответ:простой, тот же механизм динамической диспетчеризации, который используется для других функций virtual
, используется для деструкторов.Чтобы обновить его, каждый объект сохраняет указатель (vptr
) на каждый из его vtable
s (в случае множественного наследования может быть более одного), когда компилятор видит вызов любой виртуальной функции, он следуетvptr
статического типа указателя для поиска vtable
, а затем использует указатель в этой таблице для переадресации вызова.В большинстве случаев вызов может быть отправлен напрямую, в других (множественное наследование) он вызывает некоторый промежуточный код ( thunk ), который фиксирует указатель this
для ссылки на тип final overrider для этой функции.
Как компилятор затем вызывает базовые деструкторы?
Процесс разрушения объекта занимает больше операций, чем те, которые вы пишете внутри теладеструктора.Когда компилятор генерирует код для деструктора, он добавляет дополнительный код как до, так и после пользовательского кода.
Перед вызовом первой строки пользовательского деструктора компилятор вводит код, который сделает тип объекта типом вызываемого деструктора.То есть непосредственно перед вводом ~derived
компилятор добавляет код, который изменит vptr
, чтобы он ссылался на vtable
из derived
, так что эффективно, тип времени выполнения объекта становится derived
(*) .
После последней строки вашего пользовательского кода компилятор вводит вызовы деструкторов-членов, а также базовых деструкторов.Это выполняется , отключая динамическую диспетчеризацию , что означает, что она больше не будет доходить до только что выполненного деструктора.Это эквивалентно добавлению this->~mybase();
для каждой базы объекта (в обратном порядке объявления баз) в конце деструктора.
При виртуальном наследовании все становится немного сложнее, нов целом они следуют этой схеме.
РЕДАКТИРОВАТЬ (забыл (*) ): (*) Стандартные мандаты в §12 / 3:
Когда виртуальная функция вызывается прямо или косвенно из конструктора (в том числе из mem-initializer для члена данных) или из деструктора, а объект, к которому применяется вызов, является объектомв процессе конструирования или уничтожения вызываемая функция является той, которая определена в собственном классе конструктора или деструктора или в одной из его баз, но не является функцией, переопределяющей ее в классе, производном от класса конструктора или деструктора, или переопределяющей его в одномдругих базовых классов самого производного объекта.
Это требование подразумевает, что тип времени выполнения объекта - это тип cв настоящее время создается / уничтожается, даже если исходный объект, который создается / уничтожается, имеет производный тип.Простой тест для проверки этой реализации может быть:
struct base {
virtual ~base() { f(); }
virtual void f() { std::cout << "base"; }
};
struct derived : base {
void f() { std::cout << "derived"; }
};
int main() {
base * p = new derived;
delete p;
}