Управление исключением - блоки try и catch, операция throw

Предлагаемое в C++ решение проблемы реакции на синхронные исключительные ситуации связано с использованием так называемых контролируемых блоков операторов.

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

Синтаксис контролируемых блоков описывается следующим множеством формул Бэкуса-Наура:

Оператор ::= КонтролируемыйБлокОператоров

КонтролируемыйБлокОператоров ::= try СоставнойОператор СписокРеакций

СписокРеакций ::= Реакция [СписокРеакций]

Реакция ::= catch (ОбъявлениеИсключения) СоставнойОператор

ОбъявлениеИсключения ::= СписокСпецификаторовТипа Описатель
                   ::= СписокСпецификаторовТипа АбстрактныйОписатель
                   ::= СписокСпецификаторовТипа
                   ::= ...

Это одно из последних множеств БНФ на нашем пути. Всё те же знакомые описатели, любимые абстрактные описатели, и даже хорошо известное многоточие.

На основе данных БНФ строим контролируемый блок операторов. Как всегда, пока важен лишь внешний вид оператора.

КонтролируемыйБлокОператоров
try СоставнойОператор СписокРеакций
try 
 {
  Оператор
  Оператор
  Оператор
 }
СписокРеакций
try 
 {
  int retVal;
  retVal = MyFun(255);
  cout << "retVal == " << retVal << "…" << endl
 }
catch (ОбъявлениеИсключения) СоставнойОператор
СписокРеакций
try 
 {
  int retVal;
  retVal = MyFun(255);
  cout << "retVal == " << retVal << "..." << endl
 }
catch (char *) 
 {
  x = x * 25;
 }
catch (MyException MyProblem1)
 {
 cout << "Неполадки типа MyException: " << MyProblem1.text << endl;
 }
catch (...)
 {
 cout << "Нераспознанные исключения..." << endl;
 }

Итак, контролируемый блок операторов. Прежде всего, это блок операторов, то есть, составной оператор. Его место - тело функции. Этот оператор может входить в любой другой блок операторов.

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

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

catch-блок содержит код, предназначенный для перехвата исключений. Однако без генератора исключений он абсолютно бесполезен.

Возбуждение (или генерация) исключения обеспечивается операцией throw. Это весьма странная операция. Даже с точки зрения синтаксиса:

Выражение ::= ГенерацияИсключения
ГенерацияИсключения ::= throw [Выражение]

Выражение, состоящее из одного символа операции (с пустым множеством операндов) уже является выражением. Выражением с неопределённым значением. Однако оператор на основе этого выражения построить можно!

throw;

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

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

Функции unexpected и terminate в ходе выполнения программы можно заменить какие-либо другими функциями, для чего следует воспользоваться функциями set_unexpected и set_terminate. Прототипы функций set_unexpected и set_terminate обычно располагаются в заголовочном файле <except.h>. В качестве параметров они получают указатели на функции и возвращают значения адресов замещённых функций. Так что по ходу дела всё можно будет с помощью этих же функций вернуть назад.

Пользовательские функции, замещающие функции unexpected и terminate, всё равно оказываются самыми последними функциями завершаемой программы. Именно поэтому они должны иметь тип возвращаемого значения void, а также не должны иметь параметров.

Наконец, последней функцией завершаемой программы всё равно оказывается функция abort. Всевозможные ухищрения лишь откладывают момент её неизбежного вызова. Позже мы рассмотрим пример, реализующий замещение этих функций.

При автономном использовании оператора возбуждения исключения, его перехват оказывается как бы и ни при чём. Однако это всего лишь частный случай, один из самых простых вариантов сценария, к которому может привести использование операции throw.

Операция throw может применяться в сочетании с операндом, каковым может оказаться выражение произвольного типа и значения.

Оператор, построенный на основе такого выражения, можно называть генератором исключения. А его место расположения обычно называют точкой генерации. Вот примеры разнообразных генераторов исключений:

throw 1;
throw "Это сообщение об исключении…";
throw 2*2*fVal;
throw (int)5.5;
throw (ComplexType)125.96;
/*
Разумеется, если определён соответствующий конструктор
преобразования или функция приведения.
*/

Для генератора важны как значение выражения-исключения, так и его тип. Иногда даже конкретное значение исключения не так важно, как его тип.

В качестве исключения может быть использовано значение указателя. Допускаются исключения и такого вида:

throw NULL;
throw (void *) &iVal;

И, естественно, не существует быть генераторов исключений для выражений типа void. Пустой тип не имеет значений.

Обычно генератор исключения используется в сочетании с try-блоком. Их взаимодействие обеспечивается через стек вызова. Поэтому точка генерации исключения должна располагаться в теле функции, непосредственно или косвенно вызываемой из множества операторов данного try-блока. В крайнем случае, генератор исключения сам может быть одним из операторов этого try-блока.

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

Но сначала вспомним пару форм Бэкуса-Наура, посвящённых объявлению и определению функций. Речь пойдёт о спецификации исключения. С первого взгляда всё это уже кажется простым и понятным:

ОбъявлениеФункции ::=
               [СписокСпецификаторовОбъявления]
                                           Описатель
                                            [СпецификацияИсключения];

ОпределениеФункции ::=
               [СписокСпецификаторовОбъявления]
                                         Описатель
                                          [ctorИнициализатор]
                                            [СпецификацияИсключения]
                                                          ТелоФункции

СпецификацияИсключения ::= throw ([СписокТипов])

СписокТипов ::= [СписокТипов ,] ИмяТипа

Из последнего уточнения структуры объявления и определения функции следует, что объявление и определение любой функции может быть дополнено спецификацией исключения. Эта спецификация является дополнительным элементом заголовка функции и состоит из ключевого слова throw и заключённого в круглые скобки списка типов. При этом пустой список типов эквивалентен полному отсутствию спецификации исключения.

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

Назад | Содержание | Вперед