Как работает виртуальный деструктор в С++
Я наберу пример:
class A
{
public:
virtual ~A(){}
};
class B: public A
{
public:
~B()
{
}
};
int main(void)
{
A * a = new B;
delete a;
return 0;
}
Теперь в примере выше деструкторы будут называться рекурсивно снизу вверх.
Мой вопрос заключается в том, как компилятор выполняет этот MAGIC.
Ответы
Ответ 1
В вашем вопросе есть две разные части магии. Первый из них заключается в том, как компилятор вызывает конечный переопределитель для деструктора, а второй - как он затем вызывает все остальные деструкторы в порядке.
Отказ от ответственности. Стандарт не предусматривает какого-либо конкретного способа выполнения этих операций, он только определяет, каково поведение операций на более высоком уровне. Это детали реализации, которые являются общими для различных реализаций, но не соответствуют стандарту.
Как компилятор отправляет конечный перегружатель?
Первый ответ простой, тот же механизм динамической рассылки, который используется для других функций virtual
, используется для деструкторов. Чтобы обновить его, каждый объект хранит указатель (vptr
) для каждого из его vtable
(в случае множественного наследования их может быть несколько), когда компилятор видит вызов любой виртуальной функции, он следует vptr
статического типа указателя, чтобы найти vtable
, а затем использует указатель в этой таблице для переадресации вызова. В большинстве случаев вызов может быть отправлен напрямую, а в других (множественное наследование) он вызывает некоторый промежуточный код (thunk), который исправляет указатель this
, чтобы ссылаться на тип конечного переопределения для этой функции.
Как компилятор затем вызывает базовые деструкторы?
Процесс разрушения объекта требует больше операций, чем те, которые вы пишете внутри тела деструктора. Когда компилятор генерирует код для деструктора, он добавляет дополнительный код как до, так и после пользовательского кода.
Прежде чем вызывается первая строка определяемого пользователем деструктора, компилятор вводит код, который сделает тип объекта тем, что вызывается деструктором. То есть, до ввода ~derived
, компилятор добавляет код, который будет изменять vptr
, чтобы ссылаться на vtable
of derived
, так что эффективно, тип времени выполнения объекта становится derived
(*).
После последней строки вашего пользовательского кода компилятор вводит вызовы деструкторам-членам, а также базовым деструкторам. Это выполняется с отключением динамической отправки, что означает, что он больше не будет полностью отставать от только что выполненного деструктора. Это эквивалентно добавлению this->~mybase();
для каждой базы объекта (в обратном порядке объявления баз) в конце деструктора.
С виртуальным наследованием все становится немного сложнее, но в целом они следуют этому шаблону.
EDIT (забыли (*)):
(*) Стандартные мандаты в §12/3:
Когда виртуальная функция вызывается прямо или косвенно из конструктора (в том числе из mem-инициализатора для элемента данных) или из деструктора, а объект, к которому применяется вызов, является объектом, находящимся в стадии разработки или уничтожения, функция вызываемый - это тот, который определен в собственном классе конструктора или деструктора или в одной из его баз, но не в функции, переопределяющей его в классе, производном от класса конструктора или деструктора, или переопределении его в одном из других базовых классов самый производный объект.
Это требование подразумевает, что тип времени выполнения объекта является классом, который был сконструирован/разрушен в это время, даже если исходный объект, который создается/уничтожается, имеет производный тип. Простой тест для проверки этой реализации может быть:
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;
}
Ответ 2
Виртуальный деструктор обрабатывается так же, как любая другая функция virtual
. Я отмечаю, что вы правильно настроили деструктор базового класса как virtual
. Таким образом, это не отличается от любой другой функции virtual
, что касается динамической отправки. Деструктор самого производного класса вызывается через динамическую рассылку, но он также автоматически вызывает вызовы деструкторам класса Base класса 1.
Большинство компиляторов реализует эту функцию с помощью vtable
и vptr
, хотя спецификация языка не предусматривает ее. Может быть компилятор, который делает это по-другому, не используя vtable
и vptr
.
В любом случае, поскольку это верно для большинства компиляторов, стоит знать, что такое vtable
. Итак, vtable
- это таблица содержит указатели на все виртуальные функции, которые определяет класс, а компилятор добавляет vptr
к классу как скрытый указатель, который указывает на правильный vtable
, поэтому компилятор использует правильный индекс, времени, до vtable
, чтобы отправить правильную виртуальную функцию во время выполнения.
1. Текст, выделенный курсивом, берется из комментария @Als. Спасибо ему. Это делает вещи более ясными.
Ответ 3
Как обычно, с виртуальными функциями будет некоторый механизм реализации (например, указатель vtable), который позволит компилятору найти, какой деструктор запускается первым в зависимости от типа объекта. После запуска деструктора самого производного класса он, в свою очередь, запускает деструктор базового класса и т.д.
Ответ 4
Подходящая реализация (виртуальных) деструкторов, которые может использовать компилятор, будет (в псевдокоде)
class Base {
...
virtual void __destruct(bool should_delete);
...
};
void Base::__destruct(bool should_delete)
{
this->__vptr = &Base::vtable; // Base is now the most derived subobject
... your destructor code ...
members::__destruct(false); // if any, in the reverse order of declaration
base_classes::__destruct(false); // if any
if(should_delete)
operator delete(this); // this would call operator delete defined here, or inherited
}
Эта функция определяется, даже если вы не определили деструктор. В этом случае ваш код будет пустым.
Теперь все производные классы будут переопределять (автоматически) эту виртуальную функцию:
class Der : public Base {
...
virtual void __destruct(bool should_delete);
...
};
void Der::__destruct(bool should_delete)
{
this->__vptr = &Der::vtable;
... your destructor code ...
members::__destruct(false);
Base::__destruct(false);
if(should_delete)
operator delete(this);
}
Вызов delete x
, где x
имеет указатель на тип класса, будет переведен как
x->__destruct(true);
и любой другой вызов деструктора (неявный из-за переменной, выходящей из области видимости, явный x.~T()
) будет
x.__destruct(false);
В результате получается
- самый производный деструктор, который всегда вызывается (для виртуальных деструкторов)
- удалить оператор из самого производного объекта, который вызывается
- вызываемые деструкторы всех членов и базовых классов.
НТН. Это должно быть понятно, если вы понимаете виртуальные функции.
Ответ 5
Это до компилятора, как его реализовать, и обычно это делается с помощью того же механизма, что и другие виртуальные методы. Другими словами, нет ничего особенного в деструкторах, для которых требуется механизм отправки виртуального метода, отличный от того, который используется обычными методами.
Ответ 6
Виртуальный деструктор имеет запись в виртуальной таблице, как и другие виртуальные функции. Когда вызывается деструктор - либо вручную, либо автоматически от вызова до delete
- вызывается наиболее производная версия. Деструктор также автоматически вызывает деструктор для своих базовых классов, так что в сочетании с виртуальной диспетчером это вызывает магию.
Ответ 7
В отличие от других виртуальных функций, когда вы переопределяете виртуальный деструктор, виртуальный деструктор объекта вызывается в дополнение к любым унаследованным виртуальным деструкторам.
Технически это может быть достигнуто любыми средствами, которые выбирает компилятор, но почти все компиляторы достигают этого через статическую память, называемую vtable, которая допускает полиморфизм функций и деструкторов. Для каждого класса в исходном коде во время компиляции создается статическая константа vtable. Когда объект типа T создается во время выполнения, память объекта инициализируется скрытым указателем vtable, который указывает на T vtable в ПЗУ. Внутри vtable представлен список указателей на функции-члены и список указателей на функцию деструктора. Если переменная любого типа, имеющая vtable, выходит за пределы области видимости или удаляется с удалением или удалением [], все указатели деструктора в vtable, на которые указывает объект, все вызываются. (Некоторые компиляторы предпочитают хранить только самый производный указатель деструктора в таблице, а затем включать скрытый вызов деструктора суперкласса в теле каждого виртуального деструктора, если таковой существует. Это приводит к эквивалентному поведению.)
Требуется дополнительная магия для виртуального и невиртуального множественного наследования. Предположим, что я удаляю указатель p, где p относится к типу базового класса. Нам нужно вызвать деструктор подклассов с помощью this= p. Но использование множественного наследования p и начало производного объекта могут быть не такими же! Существует фиксированное смещение, которое необходимо применить. Существует одно такое смещение, хранящееся в vtable для каждого наследуемого класса, а также набор унаследованных смещений.
Ответ 8
Когда у вас есть указатель на объект, он указывает на блок памяти, который имеет как данные для этого объекта, так и "указатель vtable". В компиляторах microsoft указатель vtable является первой частью данных в объекте. В компиляторах Borland это последнее. В любом случае, это указывает на таблицу vtable, которая представляет список векторов функций, соответствующих виртуальным методам, которые могут быть вызваны для этого объекта/класса. Виртуальный деструктор - это еще один вектор в этом списке векторов указателей функций.