Обработка Исключений

advertisement
Обработка Исключений
Исключения
Исключения являются средствами С++ для отделения генерации информации о
возникновении ошибки от ее обработки.
То есть, обработка исключений должна быть разделена на две части:
1) генерация информации о возникновении ошибочной ситуации, которая
не может быть разрешена локально;
2) обработка ошибок, обнаруженных в других местах.
throw и catch
Основная идея состоит в том, что функция, обнаружившая проблему, которую она не
знает как решать, генерирует (throw) исключение в надежде, что вызывающий модуль
знает, что делать в данной ситуации:
struct Range_error {
int i;
Range_error (int ii) { i = ii; }
};
char to_char (int i) {
if (i < numeric_limits<char<::min() ||
numeric_limits<char<::max() < i)
throw Range_error (i);
return i;
}
Функция to_char() либо возвращает char по числовому значению i, либо генерирует
исключение Range_error.
throw и catch
Функция, обрабатывающая ошибку, может объявить, что она будет перехватывать
(catch) исключения данного типа:
void g (int i) {
try {
char c = to_char (i);
//...
}
catch (Range_error) {
cerr << “проблема\n”;
}
}
throw и catch
Конструкция
catch (/* ... */) {
// ...
}
называется обработчиком исключений. Она может использоваться только после блока
с ключевым словом try. В скобках используется объявление, указывающее тип
объектов, которые могут быть перехвачены данным обработчиком.
Если код в try-блоке генерирует исключение, будут проверяться обработчики этого
блока. Будет выполнен тот обработчик, чей тип совпадает со сгенерированным
исключением. Если в try-блоке исключений нет, то блок ведет себя как обыкновенный
кусок кода.
Что же считать исключением?
Механизмы обработки исключений С++ предназначены для генерации сообщения об
ошибке и обработки ошибок и исключительных событий. Задача программиста –
решить, что является исключительным в программе.
Но «исключительный» не означает «почти никогда не происходящий» или
«разрушительный». Можно рассматривать механизмы обработки исключений просто
как еще одну управляющую функцию. Например, в случае поиска по дереву:
void fnd (Tree* p, const string& s)
{
if (s == p->str) throw p;
if (p->left) fnd (p->left, s);
if (p->right) fnd (p->right, s);
}
Tree* find (Tree* p, const string& s)
{
try { fnd (p, s); }
catch (Tree* q) { return q; }
return 0;
}
// найдено
// q->str == s
Группировка исключений
Часто исключения естественным образом разбиваются на семейства. Следовательно,
наследование может помочь при структурировании исключений и их обработке.
Рассмотрим исключения для математической библиотеки:
class Matherr{};
class Overflow: public Matherr{};
//переполнение сверху
class Underflow: public Matherr{}; //переполнение снизу
class Zerodivide: public Matherr{}; //деление на ноль
Это позволит нам обрабатывать любой Matherr, не заботясь о том, какое в точности
исключение возникло:
void f ()
{
try{ /*....*/ }
catch (Overflow) { /*...*/ } // обработка Overflow и всех его производных
catch (Matherr) { /*...*/ } // обработка любой Matherr (но не Overflow! )
}
Здесь Overflow обрабатывается специальным образом. Все остальные исключения
Matherr будут обрабатываться вместе.
Группировка исключений
Организация исключений в виде иерархий может иметь большое значение для
надежности кода. В качестве примера рассмотрим, как бы могли обрабатываться
исключения в библиотеке математических функций без механизма группировки:
void g ()
{
try { /*... */ }
catch (Overflow) { /*...*/ };
catch (Underflow) { /*...*/ };
catch (Zerodivide) { /*...*/ };
}
Необходимо перечислить все исключения, однако программист может легко забыть
указать исключение в этом списке. Кроме того, при введении нового исключения в
этой библиотеке, необходимо было бы «дописывать» каждый фрагмент кода,
обрабатывающий все математические исключения.
Производные исключения.
Использование иерархий классов естественно приводит к обработчикам,
интересующимся только подмножеством информации, которой располагают
исключения. То есть, исключение зачастую перехватывается обработчиком его
базового, а не собственного класса:
class Matherr {
//...
virtual void debug_print() const { cerr << “Математическая ошибка”; }
}
class Int_overflow: public Matherr {
const char* op;
int a1, a2;
public:
int_overflow (const char* p, int a, int b) { op=p; a1=a; a2=b; }
virtual void debug_print() const { cerr<<op<<‘(’<<a1<<‘,’<<a2<<‘)’; }
//...
};
void f() {
try { g(); }
catch (Matherr m) { /*...*/ }
}
В данном случае, обработчик Matherr вызывается всегда, даже есть вызов g() привел к
генерации Int_overflow. Это означает, что дополнительная информация, имеющаяся в
Int_overflow недоступна.
Производные исключения.
Во избежание потери информации, можно использовать указатели или ссылки:
int add (int x, int y) {
if ((x>0 && y>0 && x>INT_MAX–y)||(x<0 && y<0 && x<INT_MIN–y))
throw Int_overflow (“+”, x, y);
return x+y;
}
void f() {
try {
int i1=add(1, 2);
int i2=add(INT_MAX, -2);
int i3=add(INT_MIN, 2);
}
catch (Matherr& m) {
//...
m.debug_print();
}
}
Последний вызов add() приведет к исключению, которое вызовет
Int_overflow::debug_print(). Если исключение перехватывалось бы по значению,
была бы вызвана функция Matherr:: debug_print().
Композиция исключений.
Не каждая группа исключений является древообразной структурой. Довольно часто
ошибка принадлежит сразу двум группам, например:
// ошибка, связанная с файлом в сети
class Netfile_err: public Network_err, public File_system_err { /*...*/ };
void f()
{
try { /*...*/ }
catch (Network_err& e) { /*...*/ }
}
void g()
{
try { /*...*/ }
catch (File_system_err& e) { /*...*/ }
}
Таким образом, Netfile_err может перехватываться как функциями, работающими с
исключениями в сети (Network_err), так и функциями, работающими с исключениями
файловой системы(File_system_err).
Такая неиерархическая организация обработки ошибок имеет большое значение тогда,
когда службы (например сетевые) прозрачны для пользователя. В нашем же случае,
автор g() мог и не подозревать о существовании сети.
Повторная генерация исключений
Перехватив исключение, обработчик часто решает, что он не может полностью его
обработать. В этом случае обработчик делает то, что может, после чего вновь
генерирует исключение. Таким образом, ошибка может быть обработана в наиболее
подходящем месте. Это происходит и в том случае, когда информация, требуемая для
корректной обработки ошибки, распределена по нескольким обработчикам:
void h() {
try { /* код, который может привести к математическим ошибкам */ }
catch (Matherr) {
if (can_handle_it_completely) // если может обработать полностью
// обработка Matherr
return;
}
else {
// делает то, что можно сделать здесь
throw; // повторная генерация исключения
}
}
}
Факт повторной генерации отмечается отсутствием операнда у throw. Повторно
генерируемое исключение является исходным исключением, а не просто его частью,
которая была доступна как Matherr. Если осуществляется попытка повторной
генерации при отсутствии исключения, будет вызвана функция terminate().
Перехват всех исключений
Также как и в функциях, многоточие «…» означает «любой аргумент». Поэтому
catch(...) означает «перехват всех исключений»:
void m() {
try { /* что-нибудь*/ }
catch(...) {
// очистка
throw;
}
}
То есть, если в результате выполнения m() возникает исключение, обработчик
осуществляет очистку. После того, как локальная очистка завершена, приведшее к ней
исключение повторно генерируется для дальнейшей обработки ошибки.
Но такой обработчик во многих важных случаях является не самым лучшим решением.
Порядок записи обработчиков
Из-за того, что производные исключения могут быть перехвачены обработчиками,
предназначенными для более чем одного вида исключений, чрезвычайно важен
порядок, в котором записаны обработчики в инструкции try:
void g() {
try { /*...*/ }
catch(...) { /* обработка любого исключения */ }
catch(std::exception& e) { /* обработка любого исключения стандартной библиотеки */ }
catch(std::bad_cast) { /* обработка сбоя dynamic_cast */ }
}
Обработчики проверяются по порядку. Поэтому вариант с exception никогда не будет
рассматриваться. Более того, даже если убрать обработчик «перехватывающий все»,
bad_cast по прежнему никогда не рассматривается, так как он является производным от
exception.
Управление ресурсами
В тех случаях когда функции требуется некоторый ресурс, для дальнейшего
функционирования системы очень важно, чтобы этот ресурс был правильно
освобожден. Часто такое «правильное освобождение» осуществляется той же
функцией, которая его затребовала, перед возвратом:
void use_file (const char* fn) {
FILE* f = fopen(fn, “w”);
// использование f
fclose(f);
}
Эта схема приемлема до тех пор, пока вы не поймете, что если в части
«использование f» что-нибудь произойдет, исключение может привести к выходу из
use_file без вызова fclose.
В первом приближении можно поступить следующим образом:
void use_file (const char* fn) {
FILE* f = fopen(fn, “r”);
try { /* использование f */ }
catch(...) {
fclose(f);
throw;
}
fclose(f);
}
Управление ресурсами
Проблема с предыдущим решение следующая: оно очень многословное, утомительное
и потенциально дорогостоящее. Кроме того, любое многословное и утомительное
решение в большей степени подвержено ошибкам. Существует более элегантное
решение, его общая схема выглядит так:
void acquire() {
// выделение ресурсов
// выделение ресурса 1
// …
// выделение ресурса n
// использование ресурсов
// освобождение ресурса n
// …
// освобождение ресурса 1
}
Это напоминает поведение локальных объектов, создаваемых конструкторами и
уничтожаемых деструкторами. Следовательно, проблему с освобождением ресурсов
можно решить сходным образом.
Управление ресурсами
Итак, используем объекты классов с конструкторами и деструкторами для
освобождения ресурсов. Определим класс File_ptr, который ведет себя наподобие
FILE*:
class File_ptr {
FILE* p;
public:
File_ptr(const char* n, const char* a) { p = fopen(n, a); }
File_ptr(FILE* pp) { p == pp; }
~File_ptr() { if(p) fclose(p); }
operator FILE*() { return p; }
};
Мы можем создать File_ptr либо при наличии FILE*, либо по аргументам, требуемым
для fopen(). В любом случае, File_ptr будет уничтожен в конце его области
видимости, и его деструктор закроет файл. Основная же функция сократилась до
минимума:
void use_file (const char* fn) {
File_ptr f = fopen(fn, “r”);
// использование f
}
Деструктор будет вызван независимо от того, как завершилась функция use_file.
Управление ресурсами
Наиболее часто ресурсом, запрашиваемым разного рода способами, является память:
class Y {
int* p;
void init();
public:
Y(int s) { p = new int[s]; init(); }
~Y() { delete[] p; }
// ...
};
Такая практика является распространенной и ведет к утечке памяти. Если в init() будет
сгенерированно исключение, затребованная память не будет освобождена – деструктор
не будет вызван, потому что объект сконструирован не полностью. Более безопасным
вариантом будет:
class Z {
vector<int> p;
void init();
public:
Z(int s): p(s) { init(); }
//...
};
Память, выделенная для p, теперь управляется в vector. Если init() сгенерирует
исключение, память будет освобождена при вызове деструктора (неявно) для р.
Исчерпание ресурсов
Что делать если попытка получения ресурса завершилась неуспешно? Можно
прекратить выполнение и вернуться в вызвавшую программу или попросить
вызвавшую функцию устранить проблему и продолжить:
void* operator new (size_t size) {
for (;;) {
if (void* p = malloc(size)) return p;
// попытка найти память
if (_new_handler == 0) throw bad_alloc(); // нет обработчика - прекратить
_new_handler();
// обратиться за помощью
}
}
void my_new_handler() {
int no_of_bytes_found = find_some_memory();
if (no_of_bytes_found < min_allocation) throw bad_alloc(); // «сдаемся»
}
Где-то должен быть блок try с соответствующим обработчиком. Здесь используется
_new_handler, который является указателем на функцию, задаваемую стандартной
функцией set_new_handler(). Если нужно использовать my_new_handler() в качестве
_new_handler() надо записать:
set_new_handler (&my_new_handler);
Исключения в конструкторах
Обработка исключений позволяет передать информацию о неуспешной
инициализации из конструктора. Например, простой класс Vector мог бы защититься
от запроса слишком большого количества памяти следующим образом:
class Vector {
public:
class Size {};
enum { max = 32 000 };
Vector (int sz) {
if (sz < 0 || max < sz) throw Size();
//...
}
//...
};
Код, создающий вектора, теперь может перехватывать ошибки Vector::Size:
Vector* f(int i) {
try {
Vector* p = new Vector (i);
//...
return p;
}
catch (Vector::Size) { /* обработка ошибки размера вектора */ }
}
Спецификации исключений
Иногда существует необходимость указать набор исключений, которые могут быть
сгенерированны функцией:
void f() throw(x2, x3);
Такое объявление означает, что функция f() может сгенерировать только исключения
x2, x3 и исключения, являющиеся производными от этих типов. Если во время
выполнения функция попытается сгенерировать какое-либо другое исключение, будет
вызвана std::unexpected() и далее по умолчанию std::terminate() и abort().
То есть
void f() throw(x2, x3);
эквивалентно:
void f() {
try { /*...*/ }
catch(x2) { throw; }
catch(x3) { throw; }
catch(...) { std::unexpected(); } // не возвращает управление
}
Самым важным преимуществом такой записи является то, что объявление функций
принадлежит интерфейсу, который видят те, кто ее вызывает. Кроме того, функции со
спецификацией исключений короче и проще, чем эквивалентная версия, написанная
вручную.
Спецификации исключений
Также предполагается, что функция без спецификации исключений может
сгенерировать любое исключение:
int f();
// может генерировать любое исключение
А функция, не генерирующая исключений, объявляется с пустым списком:
int g() throw();
// не генерирует исключений
Если объявление функции содержит спецификацию, то каждое объявление этой
функции (включая определение) должно иметь спецификацию с точно тем же
набором типов исключений:
int f() throw(std::bad_alloc);
int f()
// ошибка: отсутствует спецификация исключений
{ /*...*/ }
При наследовании производный класс не имеет права генерировать исключение, не
указанное в спецификации исходной функции. Аналогично для указателей: вы можете
присвоить указатель на функцию с более ограничительной спецификацией
исключений указателю на функцию с менее ограничительной, но не наоборот:
void f() throw(X);
void (*pf1)() throw(X,Y) = &f;
void (*pf2)() throw() = &f;
//правильно
//ошибка
Неожидаемые исключения
Спецификация исключений может привести к вызовам unexpected(). Как правило,
кроме как на этапе тестирования, такие вызовы нежелательны. От них можно
избавляться путем тщательной организации исключений и спецификации интерфейса
или перехватывать вызовы unexpected() таким образом, что они становятся
безвредными.
Например, все исключения хорошо определенной подсистемы Y зачастую являются
производными от некоторого класса Yerr. Тогда, при наличии определения:
class Some_Yerr: public Yerr { /*...*/ };
функция, объявленная
void f() throw (Xerr, Yerr, exception);
передаст любое Yerr вызывающей функции. То есть ни одно исключение не запустит
unexpected().
Неперехваченные исключения
Если исключение сгенерированно, но не перехваченно, вызывается функция
std::terminate(). Суть terminate() заключается в том, что вместо обработки
исключений следует иногда использовать менее изощренную технику обработки
ошибок. Например, terminate() могла бы использоваться для прекращения процесса
или для повторной инициализации системы. Функция terminate() является
радикальным средством и применяется когда стратегия восстановления после ошибок
потерпела неудачу, и пришло время перейти на другой уровень борьбы с ошибками.
По умолчанию terminate() вызывает функцию abort(). Это «умолчание» является
хорошим вариантом для большинства пользователей, особенно во время отладки.
Следует обратить внимание, что вызов функции abort() означает ненормальный выход
из программы. В ряде случаев лучше воспользоваться функцией exit() для выхода из
программы с возвращаемым значением, которое укажет внешней системе, был ли
выход нормальным или нет.
Стандартные исключения
Имя
Чем генерируется
Заголовочный файл
bad_alloc
new
<new>
bad_cast
dynamic_cast
<typeinfo>
bad_typeid
typeid
<typeinfo>
bad_exception
спецификация исключения
<exception>
out_of_range
at()
<stdexcept>
bitset<>::operator[]()
<stdexcept>
invalid_argument конструктор bitset
<stdexcept>
overflow_error
bitset<>::to_ulong()
<stdexcept>
ios_base::failure
ios_base::clear()
<ios>
Иерархия
Выводы
•
•
•
•
•
•
•
•
•
•
Используйте исключения для обработки ошибок
Используйте деструкторы для управления ресурсами
Генерируйте исключения для указания на ошибку в конструкторе
Заставьте main() перехватывать все исключения и сообщать о них
Разделяйте обычный код и код обработки ошибок
Гарантируйте, что каждый ресурс, выделенный в конструкторе, освобождается
при возникновении исключения в этом конструкторе
Управление ресурсами должно быть иерархическим
Используйте спецификацию исключений для важных интерфейсов
Библиотека не должна волевым решением прекращать выполнение программы –
ей следует сгенерировать исключение и позволить принять решение
вызывающей функции
Разрабатывайте стратегию обработки ошибок на ранних этапах проектирования
Использованные источники
• Бьерн Страуструп, «Язык программирования С++.
Специальное издание».
• http://wikipedia.org/
Download