Решить эту проблему легко: нужно объявить в базовом классе виртуальный деструктор. Тогда при удалении объектов производных классов будет происходить именно то, что нужно. Объект будет разрушен целиком, включая все его части:
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = get TimeKeeper();
...
delete ptk; // теперь работает правильно
Обычно базовые классы вроде TimeKeeper содержат и другие виртуальные функции, кроме деструктора, поскольку назначение виртуальных функций – обеспечить возможность настройки производных классов (см. правило 34). Например, в классе TimeKeeper может быть определена виртуальная функция getCurrentTime, реализованная по-разному в разных производных классах. Любой класс с виртуальными функциями почти наверняка должен иметь виртуальный деструктор.
Если же класс не имеет виртуальных функций, это часто означает, что он не предназначен быть базовым. А в таком классе определять виртуальный деструктор не стоит. Рассмотрим класс, представляющий точку на плоскости:
class Point { // точка на плоскости
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x,y;
};
Если int занимает 32 бита, то объект Point обычно может поместиться в 64-битовый регистр. Более того, такой объект Point может быть передан как 64-битовое число функциям, написанным на других языках (таких как C или FORTRAN). Если же деструктор Point сделать виртуальным, то ситуация изменится.
Для реализации виртуальных функций необходимо, чтобы в объекте хранилась информация, которая во время исполнения позволяет определить, какая виртуальная функция должна быть вызвана. Эта информация обычно представлена указателем на таблицу виртуальных функций vptr (virtual table pointer). Сама таблица – это массив указателей на функции, называемый vtbl (virtual table). С каждым классом, в котором определены виртуальные функции, ассоциирована таблица vtbl. Когда для некоторого объекта вызывается виртуальная функция, то с помощью указателя vptr в таблице vtbl ищется та реальная функция, которую нужно вызвать.
Детали реализации виртуальных функций не важны. Важно то, что если класс Point содержит виртуальную функцию, то объект этого типа увеличивается в размере. В 32-битовой архитектуре его размер возрастает с 64 бит (два целых int) до 96 бит (два int плюс vptr); в 64-битовой архитектуре он может вырасти с 64 до 128 бит, потому что указатели в этой архитектуре имеют размер 64 бита. Таким образом, добавление vptr к объекту Point увеличивает его размер на величину от 50 до 100 %! После этого объект Point уже не может поместиться в 64-битный регистр. Более того, объекты этого типа в C++ перестают выглядеть так, как аналогичные структуры, объявленные на других языках, например на C, потому что в других языках нет понятия vptr. В результате становится невозможно передавать объекты типа Point написанным на других языках программам, если только вы не учтете наличия vptr. А это уже деталь реализации, и, следовательно, такой код не будет переносимым.
Практический вывод из всего вышесказанного состоит в том, что необоснованно объявлять все деструкторы виртуальными так же неверно, как не объявлять их виртуальными никогда. Можно высказать этот совет и в таком виде: деструкторы следует объявлять виртуальными тогда, когда в классе есть хотя бы одна виртуальная функция.
Однако невиртуальные деструкторы могут стать причиной неприятностей даже при полном отсутствии в классе виртуальных функций. Например, в стандартном классе string нет виртуальных функций, но программисты временами все же используют его как базовый класс:
class SpecialString: public std::string { // плохо! std::string содержит
... // невиртуальный деструктор
};
На первый взгляд такой код может показаться безвредным, но если где-то в приложении вы преобразуете указатель на SpecialString в указатель на string, а затем выполните для этого указателя delete, то немедленно попадете в область неопределенного поведения:
SpecialString *pss = new SpecialString(“Надвигающаяся опасность”);
std::string *ps;
...
ps = pss; // SpecialString*=>std::string*
...
delete ps; // неопределенность! На практике ресурсы, выделенные
// объекту SpecialString, не будут освобождены, потому
// что деструктор SpecialString не вызывается
То же относится к любому классу, в котором нет виртуального деструктора, в частности ко всем типам STL-контейнеров (например, vector, list, set, tr1::unordered_map [см. правило 54] и т. д.). Если у вас когда-нибудь возникнет соблазн унаследовать стандартному контейнеру или любому другому классу с невиртуальным деструктором, воздержитесь! (К сожалению, в C++ не предусмотрено никакого механизма предотвращения наследования, как, скажем, final в языке Java, или sealed в C#).
Иногда может быть удобно добавить в класс чисто виртуальный деструктор. Вспомним, что чисто виртуальные функции порождают абстрактные классы, то есть классы, экземпляры которых создать нельзя. Иногда, однако, у вас есть класс, который вы хотели бы сделать абстрактным, но в нем нет ни одной пустой виртуальной функции. Что делать? Поскольку абстрактный класс предназначен для использования в качестве базового и поскольку базовый класс должен иметь виртуальный деструктор, а чисто виртуальная функция порождает абстрактный класс, то решение очевидно: объявить чисто виртуальный деструктор в классе, который вы хотите сделать абстрактным. Вот пример:
class AWOV { // AWOV = “Abstract w/o Virtuals”
public:
virtual ~AWOV() = 0; // объявление чисто виртуального
}; // деструктора
Этот класс включает в себя чисто виртуальную функцию, поэтому он абстрактный. А раз в нем объявлен виртуальный деструктор, то можно не беспокоиться о том, что деструкторы базовых классов не будут вызваны. Однако есть одна тонкость: вы должны предоставить определение чисто виртуального деструктора:
AWOV::~AWOV(){}; // определение чисто виртуального деструктора
Дело в том, что сначала всегда вызывается деструктор «самого производного» класса (то есть находящегося на нижней ступени иерархии наследования), а затем деструкторы каждого базового класса. Компилятор сгенерирует вызов ~AWOV из деструкторов производных от него классов, а значит, вы должны позаботиться о его реализации. Если этого не сделать, компоновщик будет недоволен.
Правило включения в базовые классы виртуальных деструкторов касается только полиморфных базовых классов, то есть таких, которые позволяют манипулировать объектами производных классов с помощью указателя на базовый. TimeKeeper – полиморфный базовый класс, мы ожидаем, что при наличии указателя на объект TimeKeeper сможем манипулировать объектами AtomicClock и WaterClock.
Не все базовые классы разрабатываются с учетом полиморфизма. Например, и стандартный тип string, и типы STL-контейнеров спроектированы так, что не допускают возможности использования в качестве базовых, так как не являются полиморфными. Некоторые классы предназначены служить в качестве базовых, но полиморфно использоваться не могут; примером могут служить класс Uncopyable из правила 6 и класс input_iterator_tag из стандартной библиотеки (см. правило 47). Таким классам не нужны виртуальные деструкторы.
Что следует помнить• Полиморфные базовые классы должны объявлять виртуальные деструкторы. Если класс имеет хотя бы одну виртуальную функцию, он должен иметь виртуальный деструктор.
• В классах, не предназначенных для использования в качестве базовых или для полиморфного применения, не следует объявлять виртуальные деструкторы.
Правило 8: Не позволяйте исключениям покидать деструкторы
C++ не запрещает использовать исключения в деструкторах, но это, безусловно, очень нежелательная практика. На то есть серьезная причина. Рассмотрим пример:
class Widget {
public:
...
~Widget() {...} // предположим, здесь есть исключение
};
void doSomething()
{
std::vector v;
...
} // здесь v автоматически уничтожается
Когда вектор v уничтожается, он отвечает за уничтожение всех объектов Widget, которые в нем содержатся. Предположим, что v содержит 10 объектов Widget, и во время уничтожения первого из них возбужается исключение. Остальные девять объектов Widget также должны быть уничтожены (иначе ресурсы, выделенные для них, будут потеряны), поэтому необходимо вызвать и их деструкторы. Но представим, что в это время деструктор второго объекта Widget также возбудит исключение. Тогда возникнет сразу два одновременно активных исключения, а это слишком много для C++. В зависимости от конкретных условий исполнение программы либо будет прервано, либо ее поведение окажется неопределенным. В этом примере как раз имеет место второй случай. И так будет происходить при использовании любого библиотечного контейнера (например, list, set), любого контейнера TR1 (см. правило 54) и даже массива. И причина этой проблемы не в контейнерах или массивах. Преждевременное завершение программы или неопределенное поведение здесь является результатом того, что деструкторы возбуждают исключения. C++