Как работает виртуальный деструктор в C ++ - PullRequest
11 голосов
/ 13 октября 2011

Я наберу пример:

class A
{
public:
virtual ~A(){}
};

class B: public A
{
public:
~B()
{
}

};



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

Теперь в вышеприведенном примере деструкторы будут вызываться рекурсивно снизу вверх.Мой вопрос, как компилятор делает это MAGIC.

Ответы [ 8 ]

10 голосов
/ 13 октября 2011

В вашем вопросе есть две разные части волшебства .Первый - как компилятор вызывает последний переопределитель для деструктора, а второй - как он затем вызывает все остальные деструкторы по порядку.

Отказ от ответственности: Стандарт не требует какого-либо конкретного способа выполненияэти операции, он только предписывает поведение операций на более высоком уровне.Это детали реализации, которые являются общими для различных реализаций, но не предписаны стандартом.

Как компилятор отправляет окончательную переопределение?

Первый ответ:простой, тот же механизм динамической диспетчеризации, который используется для других функций 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;
}
4 голосов
/ 13 октября 2011
Деструктор

A virtual обрабатывается так же, как любая другая функция virtual. Я отмечаю, что вы правильно сделали деструктор базового класса virtual. Как таковая, она не отличается ни от какой другой функции virtual в том, что касается динамической диспетчеризации. Деструктор самого производного класса вызывается через динамическую диспетчеризацию, но он также автоматически приводит к вызовам деструкторов Базового класса класса 1 .

Большинство компиляторов реализуют эту функцию, используя vtable и vptr, хотя спецификация языка не требует этого. Может быть компилятор, который делает это по-другому, без использования vtable и vptr.

В любом случае, поскольку это верно для большинства компиляторов, стоит знать, что такое vtable. Таким образом, vtable представляет собой таблицу, содержащую указатели всех виртуальных функций, определенных классом, и компилятор добавляет vptr к классу как скрытый указатель , который указывает на правильный vtable, поэтому компилятор использует правильный индекс, рассчитанный во время компиляции, к vtable, чтобы отправлять правильную виртуальную функцию во время выполнения.

1. Текст , выделенный курсивом взят из комментария @ Als. Спасибо ему. Это делает вещи более понятными.

2 голосов
/ 13 октября 2011

Подходящей реализацией (виртуальных) деструкторов, которые может использовать компилятор, будет (в псевдокоде)

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);

В результате

  • самый производный деструктор, который всегда вызывается (для виртуальных деструкторов)
  • оператор delete из самого производного объекта, вызываемого
  • вызываемые деструкторы всех членов и базовых классов.

НТН. Это должно быть понятно, если вы понимаете виртуальные функции.

2 голосов
/ 13 октября 2011

Как обычно, с виртуальными функциями будет какой-то механизм реализации (например, указатель vtable), который позволит компилятору определить, какой деструктор должен запускаться первым в зависимости от типа объекта.Как только деструктор самого производного класса будет запущен, он, в свою очередь, запустит деструктор базового класса и т. Д.

1 голос
/ 13 октября 2011

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

Технически это может быть достигнуто любыми средствами, выбранными компилятором, но почти все компиляторы достигают этого через статическую память, называемую vtable , которая допускает полиморфизм функций и деструкторов. Для каждого класса в вашем исходном коде статическая константа vtable генерируется для него во время компиляции. Когда объект типа T создается во время выполнения, память объекта инициализируется скрытым указателем vtable , который указывает на v-таблицу T в ПЗУ. Внутри vtable находится список указателей на функции-члены и список указателей на функции-деструкторы. Когда переменная любого типа, имеющая vtable, выходит из области видимости или удаляется с помощью delete или delete [], все указатели деструктора в vtable, на которые указывает объект, все вызываются. (Некоторые компиляторы предпочитают хранить в таблице только самый производный указатель деструктора, а затем включать скрытый вызов деструктора суперкласса в тело каждого виртуального деструктора, если таковой существует. Это приводит к эквивалентному поведению.)

Дополнительная магия необходима для виртуального и не виртуального множественного наследования. Предположим, я удаляю указатель p, где p относится к типу базового класса. Нам нужно вызвать деструктор подклассов с помощью this = p. Но при использовании множественного наследования p и начало производного объекта могут не совпадать! Существует фиксированное смещение, которое должно быть применено. В vtable хранится одно такое смещение для каждого унаследованного класса, а также набор унаследованных смещений.

1 голос
/ 13 октября 2011

Виртуальный деструктор имеет запись в виртуальной таблице, как и другие виртуальные функции.Когда деструктор вызывается - либо вручную, либо автоматически из вызова delete - вызывается самая производная версия.Деструктор также автоматически вызывает деструктор для своих базовых классов, так что в сочетании с виртуальной диспетчеризацией это вызывает magic .

1 голос
/ 13 октября 2011

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

0 голосов
/ 13 октября 2011

Когда у вас есть указатель на объект, он указывает на блок памяти, в котором есть как данные для этого объекта, так и «указатель vtable». В компиляторах Microsoft указатель vtable является первым фрагментом данных в объекте. В компиляторах Borland это последнее. В любом случае он указывает на виртуальную таблицу, которая представляет список векторов функций, соответствующих виртуальным методам, которые могут быть вызваны для этого объекта / класса. Виртуальный деструктор - это просто еще один вектор в этом списке векторов указателей на функции.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...