Естественный порядок функционирования программ нарушают возникающие нештатные ситуации, в большинстве случаев связанные с ошибками времени выполнения (но не всегда).
Обработка исключительных ситуаций носит невозвратный характер.
Носителями информации об аномальной ситуации (исключении) в C++ являются объекты заранее выбранные на эту роль типов (пользовательских или… базовых, например char*). Такие объекты называются **объектами-исключениями**. Жизненный цикл объектов-исключений начинается с возбуждения исключительной ситуации посредством оператора **throw**. При этом тип генерируемого исключения в общем случае *может* быть любым:
throw "Illegal cast"; // char*
throw IllegalCast(); // class IllegalCast
enum Status { Ok, BadIndex, IllegalCast};
throw IllegalCast; // enum Status
throw NULL; // int
throw nullptr; // pointer
Пример:
int main() {
try {
cout << "Throwing an integer exception...\n";
throw 42;
} catch (int i) {
cout << " the integer exception was caught, with value: " << i << endl;
}
return 0;
}
В языке C++ мы используем ключевое слово try для определения блока стейтментов (так называемого блока try). Блок try действует как наблюдатель в поисках исключений, которые были выброшены каким-либо из операторов в этом же блоке try.
Ключевое слово catch используется для определения блока кода (так называемого блока catch), который обрабатывает исключения определенного типа данных.
Хоть в C++ можно сгенерировать исключаение любого типа, стандартом является исключения отнаследованные от типа std::exception
. И здесь и далее должна будет использоваться именно этот стандартный подход
Так как в блоки try может быть сгенерированы разные исключения при определенных условиях, то и количество блоков catch не ограничено; в каждом из которых можно ловить свой тип исключений. Все блоки catch выполняются последовательно, и, если тип исключения соответсвует нужному блоку, то произойдет вход в него и после выход из блоков try-catch (то есть все последующие проигнорируются).
Общим типом исключений, как уже было описано является тип std::exception
. Но, если, чтобы словить совсмем все типы исключений (даже те, что не отнаследованы от std::exception
), то можно применить специальную форму - catch(…) - осуществляет перехват любых исключений.
Также тип исключений может быть как имеющий параметр исключение, так и без него. Все описанные формы представимы так:
// Именованный формальный параметр
try { /* */ } catch (const std::exception& e) { /* */ }
// Неименованный формальный параметр
try { /* */ } catch (const std::exception&) { /* */ }
// Особая форма блока-обработчика исключений осуществляет перехват любых исключений
try { /* */ } catch (...) { /* */ }
Пример:
try {
string("abc").substr(10); // генерирует std::length_error
} catch (const std::exception& e) {
cout << e.what(); // "invalid string position"
}
try {
f();
} catch (const std::exception& e) {
// будет выполнен если f() сгенерирует исключение
} catch (const std::runtime_error& e) {
// недостижимый код
}
Защищенный блок может быть оформлен не только как часть функции, но и функции целиком (в том числе main() и конструкторы классов). В таком случае защищенный блок называют функциональным:
void foobar() try {
// …
}
catch(/* … */) { /*… */ }
catch(/* … */) { /*… */ }
catch(/* … */) { /*… */ }
Поиск catсh-блока, пригодного для обработки исключения, приводит к раскрутке стека – последовательному выходу из составных операторов и определений функций. В ходе раскрутки стека происходит уничтожение локальных объектов, определенных в покидаемых областях видимости. При этом деструкторы локальных объектов вызываются штатным образом. Исключение, для обработки которого не найден catch-блок, инициирует запуск функции terminate(), передающей управление функции abort(), которая аварийно завершает программу.
Оператор throw без параметров помещается (только) в catch-блок и повторно генерирует исключение. При этом его копия не создается.
try {
f();
} catch (const std::exception& e) {
// будет выполнен если f() сгенерирует исключение и проброщено дальше по стеку
throw;
} catch (const std::runtime_error& e) {
// недостижимый код
}
Если мы заранее знаем, что конкретная функция может сгенерировать исключение мы можем помочь разработчику при вызове этих функций наверняка поместить их в try-catch блок. Достигается это добавлением к описанию функции слова throw(…) с указанием конкретных типов исключений:
int foo(int &i) throw();
bool bar(char* pc = 0) throw(IllegalCast);
void foobar() throw(IllegalCast, BadIndex);
Более того, в процессе конструирования объекта исключение может быть сгенерировано в одном из базовых классов иерархии. C++ позволяет обработать данное исключение и надлежащим образом создать объект. Для обработки всех исключений, возникших при исполнении конструктора, его тело и список инициализации должны быть совместно помещены в функциональный защищенный блок.
Пример 1:
class Alpha {
public:
Alpha(int value) { /*...*/ }
};
class Beta : public Alpha {
public:
Beta(int value);
};
Beta::Beta(int value)
try : Alpha(foo(value)) {
// ...
} catch(...) {
// ...
}
Пример 2:
struct S {
string m;
S(const string& arg) try : m(arg) {
cout << "constructed, m = " << m << endl;
} catch(const std::exception& e) {
cerr << "arg=" << arg << " failed: " << e.what() << endl;
} // throw;
};
Пример 3:
int f(int n = 2) try {
++n;
throw n;
} catch(...) {
++n;
return n;
}
Деструкторы классов не должны возбуждать исключений.
От безопасности программного кода важно отличать нейтральность, под которой, согласно терминологии Г. Саттера (Herb Sutter), следует понимать способность в методах “пропускать сквозь себя” исключения, полученные ими на обработку.
Нейтральный метод:
class exception {
public:
exception() throw();
exception(const exception&) throw();
exception& operator=(const exception&) throw();
virtual ~exception() throw();
virtual const char* what() const throw();
};
Существуют стандартные разновидности этого класса.