Не все базовые классы разрабатываются с учетом полиморфизма. Например, и стандартный тип string, и типы STL-контейнеров спроектированы так, что не допускают возможности использования в качестве базовых, так как не являются полиморфными. Некоторые классы предназначены служить в качестве базовых, но полиморфно использоваться не могут; примером могут служить класс Uncopyable из правила 6 и класс input_iterator_tag из стандартной библиотеки (см. правило 47). Таким классам не нужны виртуальные деструкторы.
Что следует помнить• Полиморфные базовые классы должны объявлять виртуальные деструкторы. Если класс имеет хотя бы одну виртуальную функцию, он должен иметь виртуальный деструктор.
• В классах, не предназначенных для использования в качестве базовых или для полиморфного применения, не следует объявлять виртуальные деструкторы.
Правило 8: Не позволяйте исключениям покидать деструкторы
C++ не запрещает использовать исключения в деструкторах, но это, безусловно, очень нежелательная практика. На то есть серьезная причина. Рассмотрим пример:
class Widget {
public:
...
~Widget() {...} // предположим, здесь есть исключение
};
void doSomething()
{
std::vector<Widget> v;
...
} // здесь v автоматически уничтожается
Когда вектор v уничтожается, он отвечает за уничтожение всех объектов Widget, которые в нем содержатся. Предположим, что v содержит 10 объектов Widget, и во время уничтожения первого из них возбужается исключение. Остальные девять объектов Widget также должны быть уничтожены (иначе ресурсы, выделенные для них, будут потеряны), поэтому необходимо вызвать и их деструкторы. Но представим, что в это время деструктор второго объекта Widget также возбудит исключение. Тогда возникнет сразу два одновременно активных исключения, а это слишком много для C++. В зависимости от конкретных условий исполнение программы либо будет прервано, либо ее поведение окажется неопределенным. В этом примере как раз имеет место второй случай. И так будет происходить при использовании любого библиотечного контейнера (например, list, set), любого контейнера TR1 (см. правило 54) и даже массива. И причина этой проблемы не в контейнерах или массивах. Преждевременное завершение программы или неопределенное поведение здесь является результатом того, что деструкторы возбуждают исключения. C++ не любит деструкторов, возбуждающих исключения!
Это достаточно просто понять. Но что вы должны делать, если в вашем деструкторе необходимо выполнить операцию, которая может породить исключение? Например, предположим, что мы имеем дело с классом, описывающим подключение к базе данных:
class DBConnection {
public:
...
static DBConnection create(); // функция возвращает объект
// DBConnection; параметры для
// простоты опущены
void close(); // закрыть соединение; при неудаче
}; // возбуждает исключение
Для гарантии того, что клиент не забудет вызвать close для объектов DBConnection, резонно создать класс для управления ресурсами DBConnection, который вызывает close в своем деструкторе. Классы, управляющие ресурсами, мы подробно рассмотрим в главе 3, а здесь достаточно прикинуть, как должен выглядеть деструктор такого класса:
class DBConn { // Класс для управления объектами
public: // DBConnection
...
~DBConn() // обеспечить, чтобы соединения с базой
{ // данных всегда закрывались
db.close();
}
private:
DBConnecton db;
};
Тогда клиент может содержать такой код:
{ // блок открывается
DBConn dbc(DBConnection::create()); // создать объект DBConnection
// и передать его объекту DBConn
... // использовать объект DBConnection
// через интерфейс DBConn
} // в конце блока объект DBConn
// уничтожается, при этом
// автоматически вызывается метод close
// объекта DBConnection
Все это приемлемо до тех пор, пока метод close завершается успешно, но если его вызов возбуждает исключение, то оно покидает пределы деструктора DBConn. Это очень плохо, потому что деструкторы, возбуждающие исключения, могут стать источниками ошибок.
Есть два основных способа избежать этой проблемы. Деструктор DBConn может:
• Прервать программу, если close возбуждает исключение; обычно для этого вызывается функция abort:
DBConn::~DBConn()
{
try {db.close();}
catch(...) {
записать в протокол, что вызов close завершился неудачно;
std::abort();
}
}
Это резонный выбор, если программа не может продолжать работу после того, как в деструкторе произошла ошибка. Преимущество такого подхода – в предотвращении неопределенного поведения. Вызов abort упредит возникновение неопределенности.