Кроме перемещения операторов инициализации членов в список инициализации пример 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.
9.4. Создание безопасных при исключениях функций-членов
Проблема
Создается функция-член и необходимо обеспечить базовые и строгие гарантии ее безопасности при исключениях, а именно отсутствие утечки ресурсов и то, что объект не будет иметь недопустимое состояние в том случае, если выбрасывается исключение.
Решение
Необходимо выяснить, какие операции могут выбрасывать исключения, и следует выполнить их первыми, обычно заключая в блок try/catch. После того как будет выполнен программный код, который может выбрасывать исключение, вы можете изменять состояние объектов. В примере 9.4 показан один из способов обеспечения безопасности функции-члена при исключениях.
Пример 9.4. Безопасная при исключениях функция-член
class Message {
public:
Message(int bufSize = DEFAULT_BUF_SIZE) :
bufSize_(bufSize), initBufSize_(bufSize), msgSize_(0), buf_(NULL) {
buf_ = new char[bufSize];
}
~Message() {
delete[] buf_;
}
// Добавить в конец символьные данные
void appendData(int len, const char* data) {
if (msgSize_+len > MAX_SIZE) {
throw out_of_range("Data size exceeds maximum size.");
}
if (msgSize_+len > bufSize_) {
int newBufSize = bufSize_;
while ((newBufSize *= 2) < msgSize_+len);
char* p = new char[newBufSize]; // Выделить память
// для нового буфера
copy(buf_, buf_+msgSize_, p); // Скопировать старые данные
copy(data, data+len, p+msgSize_); // Скопировать новые данные
msgSize_ += len;
bufSize_ = newBufSize;
delete[] buf_; // Освободись старый буфер и установить указатель на
buf_ = p; // новый буфер
} else {
copy(data, data+len, buf_+msgSize_);
msgSize_ += len;
}
}
// Скопировать данные в буфер вызывающей программы
int getData(int maxLen, char* data) {
if (maxLen < msgSize_) {
throw out_of_range("This data is too big for your buffer.");
}
copy(buf_, buf_+msgSize_, data);
return(msgSize_);
}
private:
Message(const Message& orig) {} // Мы рассмотрим эти операторы
Message& operator=(const Message& rhs) {} // в рецепте 9.5
int bufSize_;
int initBufSize_;
int msgSize_;
char* buf_;
};
Обсуждение
Представленный в примере 9.4 класс Message является классом, содержащим символьные данные; вы могли бы использовать его в качестве оболочки текстовых или бинарных данных, которые передаются из одной системы в другую. Здесь нас интересует функция-член appendData, которая добавляет данные, переданные вызывающей программой, в конец данных, уже находящихся в буфере, причем увеличивая при необходимости размер буфера. Здесь обеспечивается строгая гарантия безопасности этой функции-члена при исключениях, хотя на первый взгляд может быть не совсем понятно, чем это достигается.
Рассмотрим следующий фрагмент appendData.
if (msgSize_+len > bufSize_) {
int newBufSize = bufSize_;
while ((newBufSize *= 2) < msgSize_+len);
char* p = new char[newBufSize];
Этот блок программного кода обеспечивает увеличение размера буфера. Я его увеличиваю путем удвоения его размера до тех пор, пока он не станет достаточно большим. Этот фрагмент программного кода безопасен, потому что исключение может быть выброшено здесь только при выполнении оператора new, и я не обновляю состояние объекта и не выделяю память ни под какие другие ресурсы до завершения его выполнения. Этот оператор выбросит исключение bad_alloc, если операционная система не сможет выделить участок памяти необходимого размера.
После успешного распределения памяти я могу начать обновление состояния объекта, копируя данные и обновляя значения переменных-членов.
copy(buf_, buf_+msgSize_, p);
copy(data, data+len, p+msgSize_);
msgSize_ += len;
bufSize_ = newBufSize;
delete[] buf_;
buf_ = p;
Ни одна из этих операций не может выбросить исключение, поэтому нам не о чем волноваться. (Это происходит только из-за того, что буфер представляет собой последовательность символов; дополнительные разъяснения вы найдете при обсуждении примера 9.5.)