Правило 8: Не позволяйте исключениям покидать деструкторы

Правило 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 упредит возникновение неопределенности.

Перехватить исключение, возбужденное вызовом close:

DBConn::~DBConn()

{

try {db.close();}

catch(...) {

записать в протокол, что вызов close завершился неудачно;

}

}

Вообще говоря, такое «проглатывание» исключений – плохая идея, потому что мы теряем важную информацию: что-то не сработало ! Но иногда лучше поступить так, чтобы избежать преждевременной остановки программы или неопределенного поведения. Выбирать этот подход следует лишь в случае, когда программа в состоянии надежно продолжать исполнение, даже после того, как ошибка произошла, но была проигнорирована.

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

Более разумная стратегия – спроектировать интерфейс DBConn так, чтобы его клиенты сами имели возможность реагировать на возникающие ошибки. Например, класс DBConn может предоставить собственную функцию close и таким образом дать клиентам шанс обработать исключение, возникшее в процессе операции. Объект этого класса мог бы отслеживать, было ли соединение DBConnection уже закрыто функцией close, и, если это не так, закрывать его в деструкторе. Тем самым предотвращается утечка соединений. Но если close все-таки будет вызвана из деструктора и возбудит исключение, то мы опять возвращаемся к описанным выше вариантам: прервать программу или «проглотить» исключение:

class DBConn {

public:

...

void close() // новая функция для использования клиентом

{

db.close()

closed = true;

}

~DBConn()

{

if(!closed)

try {

db.close(); // закрыть соединение, если этого не сделал

} // клиент

catch(...) { // если возникнет исключение, запротоколировать

записать в протокол,

// и прервать программу или «проглотить» его

что вызов close

завершился неудачно;

}

}

private:

DBConnecton db;

bool closed;

};

Перемещение вызова close из деструктора DBConn в код клиента (и оставлением в деструкторе DBConn «страховочного» вызова) может показаться вам беспринципным перекладыванием ответственности. Вы даже можете усмотреть в этом нарушение принципа, описанного в правиле 18: интерфейс должно быть легко использовать правильно. На самом деле все не так. Если операция может завершиться неудачно с возбуждением исключения и есть необходимость обработать это исключение, то исключение должно возбуждаться функцией, не являющейся деструктором. Связано это с тем, что деструкторы, возбуждающие исключения, опасны и всегда чреваты преждевременным завершением программы или неопределенным поведением. Говоря клиентам, что они должны сами вызывать функцию close, мы не обременяем их лишней работой, а даем возможность обработать ошибки, на которые в противном случае они не смогли бы отреагировать. Если они считают, что им это ни к чему, то могут проигнорировать эту возможность, полагаясь на то, что соединение закроет деструктор DBConn. Если же при этом произойдет ошибка, то есть close возбудит исключение, то им не на что жаловаться, если DBConn проглотит его или прервет программу. В конце-то концов, у них ведь был случай отреагировать по-другому, а они им не воспользовались.

Что следует помнить

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

• Если клиенты класса нуждаются в возможности реагировать на исключения во время некоторой операции, то класс должен предоставить обычную функцию (то есть не деструктор), которая эту операцию выполнит.

Данный текст является ознакомительным фрагментом.



Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

2. Конструкторы и деструкторы

Из книги автора

2. Конструкторы и деструкторы Конструкторы и деструкторы являются специализированными формами методов. Используемые в связи с расширенным синтаксисом стандартных процедур New и Dispose конструкторы и деструкторы обладают способностью размещения и удаления динамических


3. Деструкторы

Из книги автора

3. Деструкторы Borland Pascal предоставляет специальный тип метода, называемый сборщиком мусора (или деструктором) для очистки и удаления динамически размещенного объекта. Деструктор объединяет шаг удаления объекта с какими-либо другими действиями или задачами, необходимыми


R.12.4 Деструкторы

Из книги автора

R.12.4 Деструкторы Деструктором называется функция-член класса cl с именем ~cl, она используется для уничтожения значений типа cl непосредственно перед уничтожением объекта, содержащего их. Деструктор не имеет формальных параметров и для него нельзя задать тип возвращаемого


R.15.3 Конструкторы и деструкторы

Из книги автора

R.15.3 Конструкторы и деструкторы Когда управление передается из точки запуска особой ситуации обработчику, то вызываются деструкторы для всех автоматических объектов, построенных с момента входа в проверяемый-блок.Если объект не был построен полностью, то деструкторы


62. Не позволяйте исключениям пересекать границы модулей

Из книги автора

62. Не позволяйте исключениям пересекать границы модулей РезюмеНе бросайте камни в соседский огород — поскольку нет повсеместно распространенного бинарного стандарта для обработки исключений С++, не позволяйте исключениям пересекать распространяться между двумя


Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе

Из книги автора

Правило 7: Объявляйте деструкторы виртуальными в полиморфном базовом классе Существует много способов отслеживать время, поэтому имеет смысл создать базовый класс TimeKeeper и производные от него классы, которые реализуют разные подходы к хронометражу:class TimeKeeper


Ода ошибкам и исключениям

Из книги автора

Ода ошибкам и исключениям Вне зависимости от того, что говорит нам наша (часто преувеличенная) самооценка, абсолютно идеальных программистов не существует. Создание программного обеспечения является сложным делом и поэтому даже самые лучшие программы могут иметь


17.4.5. Деструкторы

Из книги автора

17.4.5. Деструкторы Когда заканчивается время жизни объекта производного класса, автоматически вызываются деструкторы производного и базового классов (если они определены), а также деструкторы всех объектов-членов. Например, если имеется объект класса NameQuery:NameQuery nq( "hyperion"


17.5.5. Виртуальные деструкторы

Из книги автора

17.5.5. Виртуальные деструкторы В данной функции мы применяем оператор delete:void doit_and_bedone( vector Query* *pvec ){// ...for ( ; it != end_it; ++it ){Query *pq = *it;// ...delete pq;}}Чтобы функция выполнялась правильно, применение delete должно вызывать деструктор того класса, на который указывает pq. Следовательно,


5.5 Конструкторы и Деструкторы

Из книги автора

5.5 Конструкторы и Деструкторы Если у класса есть конструктор, то он вызывается всегда, когда создается объект класса. Если у класса есть деструктор, то он вызывается всегда, когда объект класса уничтожается. Объекты могут создаваться как:1. Автоматический объект:


7.2.6 Конструкторы и Деструкторы

Из книги автора

7.2.6 Конструкторы и Деструкторы Для некоторых производных классов нужны конструкторы. Если у базового класса есть конструктор, он должен вызыватся, и если для этого конструктора нужны параметры, их надо предоставить. Например:class base (* // ... public: base(char* n, short t); ~base(); *);class derived :


8.5.7 Деструкторы

Из книги автора

8.5.7 Деструкторы Функция член класса cl с именем ~cl называется деструтором. Деструктор не возвращает никакого значения и не полчает никаких параметров; он используется для уничтожения знчений типа cl непосредственно перед уничтожением содержащего их объекта. Деструктор не


30. Конструкторы и деструкторы

Из книги автора

30. Конструкторы и деструкторы Конструкторы и деструкторы являются специализированными формами методов. Используемые в связи с расширенным синтаксисом стандартных процедур New и Dispose конструкторы и деструкторы обладают способностью размещения и удаления динамических


31. Деструкторы

Из книги автора

31. Деструкторы Borland Pascal предоставляет специальный тип метода, называемый сборщиком мусора (или деструктором) для очистки и удаления динамически размещенного объекта. Деструктор объединяет шаг удаления объекта с какими-либо другими действиями или задачами, необходимыми


Не позволяйте рыночным крикунам сбивать вас с толку

Из книги автора

Не позволяйте рыночным крикунам сбивать вас с толку Мы видели, что в отношении средств массовой информации и коммуникации распространяется много неправды и используется тактика напускания тумана. Что можно предпринять против этого? Не позволяйте сбивать себя с толку!