Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Программирование на C / C++ / C++ for real programmers.pdf
Скачиваний:
262
Добавлен:
02.05.2014
Размер:
2.04 Mб
Скачать

Исключения

4

 

Если ваши программы всегда работают без малейших проблем, можете смело пропустить эту главу. А если нет — давайте поговорим о том, как обрабатывать исключения.

Базовый принцип, на котором основана обработка исключений, — восстановление состояния и выбор альтернативных действий в случае ошибки. Предположим, в вашей программе имеется некий блок и вы не уверены, что он доработает до конца. При выполнении блока может возникнуть нехватка памяти, или начнутся проблемы с коммуникациями, или нехороший клиентский объект передаст неверный параметр. Разве не хотелось бы написать программу в таком виде:

if (блок будет работать) { блок;

}

else {

сделать что-то другое;

}

Иначе говоря, вы заглядываете в хрустальный шар. Если в нем виден фрагмент программы, который горит синим пламенем, вы изменяете будущее и обходите этот фрагмент. Не стоит с затаенным дыханием ожидать появления таких языков программирования в ближайшем будущем, но на втором месте стоит обработка исключений. С помощью исключений вы «допрашиваете» подозрительный блок. Если в нем обнаружится ошибка, компилятор поможет восстановить состояние перед выполнением блока и продолжить работу.

Обработка исключений в стандарте ANSI

Хорошая новость: для обработки исключений существует стандарт ANSI — или, как это всегда бывает в C++, предложенный стандарт. Интересно, почему у нас так много предложенных стандартов и ни одного реального? Скорее всего, дело в том, что наша экономика не может вместить всех безработных членов комитетов стандартизации. Лучше оставить им занимательное пожизненное хобби, пока мы будем выполнять свою работу. Впрочем, я отвлекся.

Плохая новость: стандартная обработка исключений все еще не поддерживается многими компиляторами C++. Хорошая новость: все больше и больше компиляторов выходит на передовые позиции. Плохая новость: осталось немало старого кода, предназначенного для старых компиляторов. Увы.

Давайте сначала поговорим о том, как все должно происходить, а уже потом займемся теми вариациями, которые встречаются в реальном мире.

Синтаксис инициирования исключений

Следующая функция шлепнет вас по рукам, если вызвать ее с неверным параметром. Вместо линейки она воспользуется секцией throw. В этой функции могут произойти две ошибки, представленные константами перечисления Gotcha.

enum Gotcha { kTooLow, kTooHigh }; void fn(int x) throw(Gotcha) {

64

if (x

<

0)

 

throw

kTooLow;

// Функция завершается здесь

if (x

>

1000)

 

throw

kTooHigh;

// Или здесь

// Сделать что-то осмысленное

}

В первой строке определяется тип исключения. Исключения могут иметь любой тип: целое, перечисление, структура и даже класс. Во второй строке объявляется интерфейс функции с новым придатком — спецификацией исключений, который определяет, какие исключения могут быть получены от функции вызывающей стороной. В данном примере инициируется исключение единственного типа Gotcha. В четвертой и шестой строке показано, как инициируются исключения, которые должны быть экземплярами одного из типов, указанного в спецификации исключений данной функции. Спецификации исключений должны подчиняться следующим правилам.

Объявления и определения

Спецификация исключений в объявлении функции должна точно совпадать со спецификацией в ее определении.

void Fn() throw(int); // Объявление

// Где-то в файле .cpp void Fn() throw(int) { // Реализация

}

Если определение будет отличаться от объявления, компилятор скрестит руки на груди и откажется компилировать определение.

Функции без спецификации исключений

Если функция не имеет спецификации исключений, она может инициировать любые исключения. Например, следующая функция может инициировать что угодно и когда угодно.

void fn();

// Может инициировать исключения любого типа

Функции, не инициирующие исключений

Если список типов в спецификации пуст, функция не может инициировать никакие исключения. Разумеется, при хорошем стиле программирования эту форму следует использовать всюду, где вы хотите заверить вызывающую сторону в отсутствии инициируемых исключений.

void fn() throw();

// Не инициирует исключений

Функции, инициирующие исключения нескольких типов

В скобках можно указать произвольное количество типов исключений, разделив их запятыми.

void fn() throw(int, Exception_Struct, char*);

Передача исключений

Если за сигнатурой функции не указан ни один тип исключения, функция не генерирует новые исключения, но может передавать дальше исключения, полученные от вызываемых ею функций.

void fn() throw;

Исключения и сигнатуры функций

Спецификация исключений не считается частью сигнатуры функции. Другими словами, нельзя иметь две функции с совпадающим интерфейсом за исключением (нечаянный каламбур!) спецификации исключений. Две следующие функции не могут сосуществовать в программе:

typedef...
set_unexpected()

65

void

f1(int)

throw();

 

void

f1(int)

throw(Exception);

// Повторяющаяся сигнатура!

Спецификация исключений для виртуальных функций

В главе 2 мы говорили (точнее, я говорил, а вы слушали) об отличиях между перегрузкой (overloading) и переопределением (overriding). Если виртуальная функция в производном классе объявляется с новой сигнатурой, отсутствующей в базовом классе, эта функция скрывает все одноименные функции базового класса (если вы в чем-то не уверены, вернитесь к соответствующему разделу; это важно понимать). Аналогичный принцип действует и для спецификаций исключений.

class Foo {

 

public:

 

virtual Fn() throw(int);

 

};

 

class Bar : public Foo {

 

public:

 

virtual Fn() throw(char*);

// Осторожно!

};

 

Компилятор косо посмотрит на вас, но откомпилирует. В результате тот, кто имеет дело с Foo*, будет ожидать исключения типа int, не зная, что на самом деле он имеет дело с объектом Ваr, инициирующим нечто совершенно иное.

Мораль ясна: не изменяйте спецификацию исключений виртуальной функции в производных классах. Только так вам удастся сохранить контракт между клиентами и базовым классом, согласно которому должны инициироваться только исключения определенного типа.

Непредусмотренные исключения

Если инициированное исключение отсутствует в спецификации исключений внешней функции, программа переформатирует ваш жесткий диск. Шутка. На самом деле она вызывает функцию с именем unexpected(). По умолчанию затем вызывается функция terminate(), о которой будет рассказано ниже, но вы можете сделать так, чтобы вызывалась ваша собственная функция. Соответствующие интерфейсы из заголовочного файла except.h выглядят так:

typedef void (*unexpected_function)();

unexpected_function set_unexpected(unexpected_function excpected_func);

В строке объявляется интерфейс к вашей функции. Функция получает функцию этого типа и организует ее вызов вместо функции по умолчанию. Функция

set_unexpected() возвращает текущий обработчик непредусмотренных исключений. Это позволяет временно установить свой обработчик таких исключений, а потом восстановить прежний. В следующем фрагменте показано, как используется этот прием.

unexpected_function my_handler(void) { // Обработать неожиданное исключение

}

{// Готовимся сделать нечто страшное и устанавливаем свой обработчик unexpected_function old_handler = set_unexpected(my_handler);

// Делаем страшное и возвращаем старый обработчик set_unexpected(old_handler);

}

Функция-обработчик не может нормально возвращать управление вызывающей программе, если в ней встречается оператор return или при выходе из области действия функции результаты будут неопределенными. Тем не менее, из функции можно запустить исключение и продолжить поиск перехватчика, подходящего для нового исключения.

typedef...

66

Синтаксис перехвата исключений

Чтобы перехватить исключение, поставьте перед блоком ключевое слово try и поместите после него одну или несколько секций catch, которые называются обработчиками (handlers).

try {

// Фрагмент, который может инициировать исключения

}

catch (Exception_Type t) {

// Восстановление после исключения типа Exception_Type

}

catch (...) {

// Восстановление после исключений всех остальных типов

}

Каждый обработчик, за исключением (опять нечаянный каламбур) обработчика с многоточием, соответствует одному конкретному типу ошибок. Если из фрагмента, называемого try-блоком, инициируется исключение, компилятор просматривает список обработчиков в порядке их перечисления и ищет обработчик, подходящий по типу запущенного исключения. Многоточие соответствует исключениям любого типа; если такой обработчик присутствует, он должен находиться последним в списке.

Внутри обработчика вы можете предпринимать любые действия для выхода из ситуации. Сведения об исключении можно получить из аргумента catch — кроме обработчика с многоточием, который понятия не имеет, что он должен перехватывать.

Выполнение программы после исключения

Если выполнение try-блока обходится без исключений, программа благополучно игнорирует все обработчики и продолжает работу с первого выражения за последним обработчиком. Если же исключение все же произошло, оно будет единственным из всего списка, и после его обработки выполнение программы продолжится за последним обработчиком списка. Существуют два исключения (последний нечаянный каламбур): обработчик может содержать вызов крамольного goto или запустить исключение. Если обработчик инициирует исключение, он может продолжить распространение того же исключения или создать новое.

catch(int exception) {

// Сделать что-то, а затем

throw(“Help!”);

// Инициируется исключение типа char*

}

Инициирование исключения из обработчика немедленно завершает выполнение вмещающей функции или блока.

Если исключение не перехвачено

Если для исключения не найдется ни одного обработчика, по умолчанию вызывается глобальная функция terminate(). Как вы думаете, что она делает? По умолчанию terminate() в конечном счете вызывает библиотечную функцию abort(), и дело кончается аварийным завершением всей программы. Вы можете вмешаться и установить собственную функцию завершения с помощью библиотечной функции set_terminate(). Соответствующий фрагмент файла except.h выглядит так:

typedef void (*terminate_function)();

termination_function set_terminate(terminate_function t_func);

В строке объявляется интерфейс к вашей функции завершения. Функция set_terminate() устанавливает функцию завершения, которую вместо функции abort() вызывает функция terminate(). Функция set_terminate() возвращает текущую функцию завершения, которую позднее можно восстановить повторным вызовом set_terminate().