Правилом хорошего тона в С++ является использование списка инициализации для вызова конструкторов членов класса, например:
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
, что значит, что исключение было обработано дважды: в теле try-блока конструктора и затем в функции 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
блока на уровне функции.
Если хоть один элементов класса при конструировании выбросил исключение, то весь класс принудительно завершает собственное конструирование с ошибкой в форме исключения вне зависимости от того, было это исключение поймано в конструкторе или нет.