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