class A { ... };
class B {
public:
B(int n);
private:
A __a;
};
B::B(int n)
: __a(n) // вызов конструктора А() в списке инициализации.
{}
А что произойдет, если в одном из вызовов в списке инициализации произойдет исключение? Например: class A {
public:
A(int n) {
throw 0; // Конструктор класса А бросает исключение int
}
};
class B {
public:
B(int n);
private:
A __a;
};
B::B(int n)
: __a(n) // Данный вызов бросает исключение
{}
Хотелось бы иметь возможность поймать это исключение и провести "чистку" уже распределенной на тот момент памяти, например: class P { ... };
class A {
public:
A(int n) {
throw 0; // Конструктор класс А бросает исключение int
}
};
class B {
public:
B();
private:
P* __p;
A __a;
};
B::B()
: __p(new P), // Память для P распределяется до вызова конструктора класса А
__a(0) // Данный вызов бросает исключение
{}
На момент, когда конструктор А
бросит исключение, мы уже будем иметь распределенную память под указателем __p
и, не обработав исключение, эту память можно потерять. В С++ есть форма задания try-catch
блока на уровне функции. Используя ее, можно переписать пример так:
#include <iostream>
class A {
public:
A(int n) {
throw 0; // Конструктор класс А бросает исключение int
}
};
class P {
public:
P() { std::cout << "P(), constructor" << std::endl; }
~P() { std::cout << "~P(), destructor" << std::endl; }
};
class B {
public:
B();
private:
P* __p;
A __a;
};
B::B()
try
: __p(new P), __a(0) {
} catch (int& e) {
std::cout << "B(), exception " << e << std::endl;
delete __p;
};
int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), exception " << e << std::endl;
}
}
Видно (см. тело конструктора B::B()
), что лист инициализации ушел между словом try
и началом try-блока, а тело конструктора теперь внутри try-блока (в данном примере оно пустое), а обработчик исключения находится в catch-блоке после тела конструктора. Данный пример сумеет обработать исключение класса А
и освободит память из под указателя __p
. Данный пример выведет следующее: P(), constructor
B(), exception 0
~P(), destructor
main(), exception 0
Видно, что деструктор класса P
был вызван. Внимательный читатель заметит, что в функции main()
тоже есть try-блок, а последней строкой программа печатает "main(), exception 0
main()
. Почему?
Правило гласит: исключение, пойманное в обрамляющем функцию виде try-catch блоке конструктора, будет переброшено еще раз при выходе из конструктора, если конструктор принудительно не сделал это сам, поймав это исключение. Сейчас очень важный момент: если хоть один из членов класса бросил исключение в процессе конструирования, то весь объект принудительно завершает конструирование аварийно с исключением вне зависимости от того, обработано это исключение в конструкторе или нет.Единственное, что мы тут можем сделать, это "на лету" подправить исключение, брошенное членом класса (например, добавить туда дополнительную информацию). Следующий пример меняет код брошенного классом
А
исключения: #include <iostream>
class A {
public:
A(int n) {
throw 0; // Конструктор класс А бросает исключение int
}
};
class B {
public:
B();
private:
A __a;
};
B::B()
try
: __a(0) {
} catch (int& e) {
std::cout << "B(), exception " << e << std::endl;
e = 1; // Меняем код исключения с 0 на 1.
};
int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), exception " << e << std::endl;
}
}
Эта программы выведет следующее: B(), exception 0
main(), exception 1
Видно, что когда исключение было поймано второй раз, код у него уже не 0 как в оригинальном исключении, а 1. С конструкторами вроде разобрались. Перейдем к деструкторам.
Деструктор — это тоже функция. К нему тоже применим синтаксис ловли исключения на уровне тела функции, например:
#include <iostream>
class B {
public:
~B();
};
B::~B()
try {
throw 2;
} catch (int& e) {
std::cout << "~B(), exception " << e << std::endl;
}
Поведение ловли исключения в деструкторе на уровне функции схоже с конструктором, то есть исключение, пойманное в catch-блоке на уровне функции будет переброшено автоматически снова при завершении деструктора, если он это не сделал сам, обработав исключение. Например: #include <iostream>
class B {
public:
~B();
};
B::~B()
try {
throw 2;
} catch (int& e) {
std::cout << "~B(), exception " << e << std::endl;
}
int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), B(), exception " << e << std::endl;
}
}
выведет: ~B(), exception 2
main(), B(), exception 2
то есть исключение, после его обработки в деструкторе было переброшено снова. Конечно, не пойманные исключения в деструкторе являются большим "no-no!" в С++. Принято считать, что не пойманное в деструкторе исключение — это прямой путь к аварийному завершению программы, так как нарушается принцип целостности системы исключений. Если хотите, чтобы ваши программы на С++ работали стабильно, то не допускайте, чтобы исключения “вылетали” из деструктора. Например так: #include <iostream>
class B {
public:
~B();
};
B::~B() {
try {
throw 2; // Бросаем исключение.
} catch (int& e) { // И тут же ловим его, не пропуская него “на волю”.
std::cout << "~B(), exception " << e << std::endl;
}
}
int main(int argc, char* argv[]) {
try {
B b;
} catch (int& e) {
std::cout << "main(), B(), exception " << e << std::endl;
}
}
Эта программа выведет: ~B(), exception 2
Видно, что исключение не дошло до функции main()
. С деструкторами тоже вроде разобрались. Теперь перейдем к обычным функциям.
Технику обработки исключений на уровне функции можно применять для любой функции, а не только для конструктора или деструктора, например:
void f()
try {
throw 1;
} catch (int& e) {
std::cout << "f(), exception " << e << std::endl;
}
Но целесообразность такого синтаксиса сомнительна, так как пойманное исключение не перебрасывается автоматически снова после окончания функции, как это было в случае с конструктором и деструктором. Программа: #include <iostream>
void f()
try {
throw 1;
} catch (int& e) {
std::cout << "f(), B(), exception " << e << std::endl;
}
int main(int argc, char* argv[]) {
try {
f();
} catch (int& e) {
std::cout << "main(), f(), B(0), exception " << e << std::endl;
}
}
напечатает только: f(), B(), exception 1
то есть исключение не было передано дальше, поэтому разумнее было бы просто оформить функцию традиционным образом с помощью try-блока, обрамляющего всё тело функции: void f() {
try {
throw 1;
} catch (int& e) {
std::cout << "f(), B(), exception " << e << std::endl;
}
}
не внося в форматирование текста лишней каши непривычным положением слов try
и catch
. Лично мне кажется, из всего выше написанного, реально для применения только try-catch блок на уровне функции для конструктора. Там это действительно актуально, чтобы не допустить объектов, сконструированных только наполовину и убитых в процессе создания исключением от собственного члена (простите за каламбур).Выводы
Исключения, брошенные при обработке списка инициализации класса можно поймать в теле конструктора через синтаксис try-catch блока на уровне функции.
Если хоть один элементов класса при конструировании выбросил исключение, то весь класс принудительно завершает собственное конструирование с ошибкой в форме исключения вне зависимости от того, было это исключение поймано в конструкторе или нет.
Уродовать try/catch список инициализации никоим образом неможно. Везде нужно использовать RAII и спать спокойно.
ОтветитьУдалитьВыводить исключения из деструктора на экран?
Мое мнение -- исключение в деструкторе должно аварийно завершать работу программы. Один из способов -- писать спецификацию исключений throw().
А как быть, если в приведенном мной примере класс А является сторонним (мы не можем его поменять) и известно, что он может бросать исключения в конструкторе? Переносить создание члена "а" и всех зависимых от его возможного исключения при конструировании членов в тело конструктора и там ловить исключения? как RAII тут может помочь?
ОтветитьУдалитьПечать на экран в данных примерах сделана исключительно в демострационных целях.
Что значит "исключение в деструкторе должно аварийно завершать работу программы"? То есть я НЕ должен ловить каких-либо исключений в деструкторе? Это как-то неверно. Например, класс tcp-сервер. Один из его членов - слушаюший сокет. Когда деструктор tcp-сервера вызовет деструктор сокета последний может метнуть исключение, так как, например, физическое tcp соединение может быть уже разорвано и close/closesocket/shutdown могут вернуть ошибку. Но исключение в разрушении сокета никак не должно влиять на деструктор сервера. Просто это исключение ловится и деструктор сервера нормально завершается.
При обработке исключений конструктора есть одно НО: в блоке catch нельзя делать delete __p равно как и delete любых других указателей, ввиду невозможности узнать был ли объект P сконструирован к моменту исключения ( веть может исключение произошло на инструкции __p(new P).
ОтветитьУдалитьт.е. в обработчике исключений конструктора никакие объекты удалять нельзя
Так вроде для подобных случаев синтаксис try-catch блока на уровне функции и придумали. Список инициализации обрабатывается последовательно, значит можно по типу брошеного исключения понять, кто именно его бросил. А затем удалить всех, кто уже был создан до проблемного объекта.
ОтветитьУдалитьВо фразе "то есть исключение не было передано дальше, поэтому разумнее было бы просто оформить функцию традиционным образом с помощью try-блока, обрамляющего всё тело функции" есть намек на то что, при оформлении блока традиционным образом исключение будет передано (в любом случае), я даже немного испугался за свои познания. Поправьте, а то это может кого запутать.
ОтветитьУдалитьЯ раз десять перечитал абзац, пытась как-то его улучшить. Не получается у меня таки уловить тут смысл, где намекается на возможность безусловного переброса исключения в обычных функциях. Тут наоборот говорится, что "обычный" синтаксис для исключений предпочтительнее для обычных функций, так как он выглядить привычнее, а ведет себя точно также, как и синтаксис try-блока-обрамления функции.
ОтветитьУдалитьЕсть вариант, как это можно написать получше?