9.3. Создание безопасного при исключениях списка инициализации
9.3. Создание безопасного при исключениях списка инициализации
Проблема
Необходимо инициализировать ваши данные-члены в списке инициализации конструктора, и поэтому вы не можете воспользоваться подходом, описанным в рецепте 9.2.
Решение
Используйте специальный формат блоков try и catch, предназначенный для перехвата исключений, выбрасываемых в списке инициализации. Пример 9.3 показывает, как это можно сделать.
Пример 9.3. Обработка исключений в списке инициализации
#include <iostream>
#include <stdexcept>
using namespace std;
// Некоторое устройство
class Device {
public:
Device(int devno) {
if (devno == 2)
throw runtime error("Big problem");
}
~Device() {}
private:
Device();
};
class Broker {
public:
Broker (int devno1, int devno2)
try : dev1_(Device(devno1)), // Создать эти объекты в списке
dev2_(Device(devno2)) {} // инициализации
catch (...) {
throw; // Выдать сообщение в журнал событий или передать ошибку
// вызывающей программе (см. ниже обсуждение)
}
~Broker() {}
private:
Broker();
Device dev1_;
Device dev2_;
};
int main() {
try {
Broker b(1, 2);
} catch(exception& e) {
cerr << "Exception: " << e.what() << endl;
}
}
Обсуждение
Синтаксис обработки исключений в списках инициализации немного отличается от традиционного синтаксиса С++, потому что здесь блок try используется в качестве тела конструктора. Критической частью примера 9.3 является конструктор класса Broker.
Broker(int devno1, int devno2) // Заголовок конструктора такой же Constructor
try : // Действует так же, как try {...}
dev1_(Device(devno1)), // Затем идут операторы списка
dev2_(Device(devno2)) { // инициализации
// Здесь задаются операторы тела конструктора.
} catch (...) { // catch обработчик задается *после*
throw; // тела конструктора
}
Режим работы блоков try и catch вполне ожидаем; единственное синтаксическое отличие от обычного блока try заключается в том, что при перехвате исключений, выброшенных из списка инициализации, за ключевым словом try идет двоеточие, затем список инициализации и после этого собственно блок try, который является одновременно и телом конструктора. Если какое-нибудь исключение выбрасывается из списка инициализации или из тела конструктора, оно будет перехвачено catch-обработчиком, который расположен после тела конструктора. Вы можете при необходимости добавить в тело конструктора дополнительную пару блоков try/catch, однако вложенные блоки try/catch обычно выглядят непривлекательно.
Кроме перемещения операторов инициализации членов в список инициализации пример 9.3 отличается от примера 9.2 еще одним свойством. Объекты-члены Device на этот раз не создаются в динамической памяти с помощью оператора new. Я сделал это для иллюстрации двух особенностей, связанных с безопасностью и применением объектов-членов.
Во-первых, использование стека вместо объектов динамической памяти позволяет компилятору автоматически обеспечить их безопасность. Если какой-нибудь объект в списке инициализации выбрасывает исключение в ходе конструирования, занимаемая им память автоматически освобождается по мере раскрутки стека в процессе обработки исключения. Во-вторых, что более важно, любые другие объекты, которые уже были успешно сконструированы, уничтожаются, и вам не требуется перехватывать исключения и явно их удалять оператором delete.
Но, возможно, вам требуется иметь члены, использующие динамическую память (или с ними вы предпочитаете иметь дело). Рассмотрим подход, используемый в первоначальном классе Broker в примере 9.2. Вы можете просто инициализировать ваши указатели в списке инициализации, не так ли?
class BrokerBad {
public:
BrokerBad(int devno1, int devno2)
try : dev1_(new Device(devno1)), // Создать объекты динамической
dev2_(new Device(devno2)) {} // памяти в списке инициализации
catch (...) {
if (dev1_) {
delete dev1_; // He должно компилироваться и
delete dev2_; // является плохим решением, если
} // все же будет откомпилировано
throw; // Повторное выбрасывание того же самого исключения
}
~BrokerBad() {
delete dev1_;
delete dev2_;
}
private:
BrokerBad();
Device* dev1_;
Device* dev2_;
};
Нет, так делать нельзя. Здесь две проблемы. Прежде всего, это не допустит ваш компилятор, потому что расположенный в конструкторе блок catch не должен позволить программному коду получить доступ к переменным-членам, так как их еще нет. Во-вторых, даже если ваш компилятор позволяет это делать, это будет плохим решением. Рассмотрим ситуацию, когда при конструировании объекта dev1_ выбрасывается исключение. Ниже дается программный код, который будет выполняться в catch-обработчике.
catch (...) {
if (dev1_) { // Какое значение имеет эта переменная?
delete dev1_; // в данном случае вы удаляете неопределенное значение
delete dev2_;
}
throw; // Повторное выбрасывание того же самого исключения
}
Если исключение выбрасывается в ходе конструирования dev1_, то оператором new не может быть возвращен адрес нового выделенного участка памяти и значение dev1_ не меняется. Тогда что эта переменная содержит? Она будет иметь неопределённое значение, так как она никогда не инициализировалась. В результате, когда вы станете выполнять оператор delete dev1_, вы, вероятно, попытаетесь удалить объект, используя недостоверный адрес, что приведет к краху программы, вы будете уволены, и вам придется жить с этим позором всю оставшуюся жизнь.
Чтобы избежать такое фиаско, круто изменяющее вашу жизнь, инициализируйте в списке инициализации ваши указатели значением NULL и затем создавайте в конструкторе объекты, использующие динамическую память. В этом случае будет легче перехватывать любую исключительную ситуацию и выполнять подчистку, поскольку допускается использовать оператор delete для NULL-указателей.
BrokerBetter(int devno1, int devno2) :
dev1_(NULL), dev2_(NULL) {
try {
dev1_ = new Device(devno1);
dev2_ = new Device(devno2);
} catch (...) {
delete dev1_; // Это сработает в любом случае
throw;
}
}
Итак, вышесказанное можно подытожить следующим образом: если вам необходимо использовать члены-указатели, инициализируйте их значением NULL в списке инициализации и затем выделяйте в конструкторе память для соответствующих объектов, используя блок try/catch. Вы можете освободить любую память в catch-обработчике. Однако, если допускается работа с автоматическими членами, сконструируйте их в списке инициализации и используйте специальный синтаксис блока try/catch для обработки любых исключений.
Смотри также
Рецепт 9.2.