C++.Бархатный путь

       

Работа системы управления исключением


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

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

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

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

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

И если исключительная ситуация возникает в цикле - пусть её перехватчик остановит цикл. А вопросы эффективности и корректной работы со стеком - это вопросы к транслятору.

#include iostream.h #include string.h /* "Рабочее тело" одного из исключений. На его основе создаётся объект исключения. */ class MyException { public: int CopyKey; char *ExcMessage; // Конструктор умолчания. MyException(): ExcMessage("Стандартное сообщение от MyException...") { CopyKey = 0; } // Конструктор копирования. MyException(const MyException MyExcKey) { cout "Работает конструктор копии..." endl; ExcMessage = strdup(MyExcKey.ExcMessage); CopyKey = 1; // Признак копии для деструктора. } // Деструктор освобождает динамическую память. ~MyException() { if (CopyKey ExcMessage) delete(ExcMessage); } }; int MyFun() throw (int, char *); int Fun2() throw (int); void main() throw (MyException) { int RetMainVal; for (RetMainVal = 0; RetMainVal = 0; ) { try { RetMainVal = MyFun(); cout "RetMainVal == " RetMainVal endl; if (RetMainVal == 9) throw MyException(); /* Вызов конструктора для создания безымянного объекта - представителя класса MyException в точке возбуждения исключения (с использованием выражения явного преобразования типа). После этого код, расположенный ниже точки генерации исключения уже не выполняется. */ cout "Последний RetMainVal не был равен 9!" " Иначе были бы мы здесь..." endl; } // Место расположения перехватчиков исключений. catch (int ExcVal) { cout "(int) ExcVal == " ExcVal endl; } catch (char *ExcMessage) { cout "(char *) ExcMessage " ExcMessage endl; } catch (MyException ExcObj) /*


Безымянный объект, созданный в точке возбуждения исключения, инициализирует параметр обработчика исключения. С этой целью нами был определен специальный конструктор копирования. */ { cout ExcObj.ExcMessage "... Такое вот сообщение пришло" endl; /* После завершения выполнения блока обработки исключения, параметр обработчика уничтожается. Для этого мы определили собственную версию деструктора. */ } cout "За пределами tryБлока: RetMainVal == " RetMainVal endl; // cout ExcMessage "!!!" endl; // Обработчик исключений определяет собственную область действия. // ExcMessage оказывается за пределами области действия имени. } cout "Это конец работы программы." " И чтобы больше никаких перехватов..." endl; } int MyFun() throw (int, char *) { int Answer, RetMyFunVal; cout "MyFun "; cin Answer; cout Answer endl; switch (Answer) { case 1: throw 1; cout "Когда рак на горе свистнет, тогда это сообщение появится."; break; case 2: throw "XXX"; case 3: RetMyFunVal = Fun2(); cout "Вернулись из Fun2(). RetMyFunVal = " RetMyFunVal endl; break; } cout "Привет из MyFun..." endl; return Answer; } int Fun2() throw (int) { int Answer; cout "Fun2 "; cin Answer; cout Answer endl; switch (Answer) { case 0: throw 1; /* После возбуждения исключения, процесс нормального выполнения программы прерывается. Мы уже не попадаем в точку возврата функции. Используя стек, минуем функцию MyFun и оказываемся непосредственно в catch-блоке функции main, связанном с исключением типа int. */ default: Answer *= 2; } cout "Конец работы в Fun2." endl; return Answer; }

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

Функция возвращает значение определённого типа. Тип возвращаемого значения является важной характеристикой функции. Спецификация возвращаемого значения явным образом указывается при объявлении и определении функции. В различных ситуациях та же функция может возбуждать исключения совершенно разных типов и классов. Средством контроля над типами возбуждаемых исключений как раз является спецификация исключений. Этот необязательный элемент в заголовке обеспечивает дополнительный контроль над функцией со стороны транслятора. Хотя функция и может без предварительной спецификации возбуждать любые исключения, им не следует пренебрегать.

Транслятор следит за тем, чтобы не нарушались области действия имён объектов. Областью действия переменной, объявленной непосредственно в try-блоке, является данный try-блок. Соответственно, областью действия переменной, объявленной в одном из catch-блоков, этот самый catch-блок.

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

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

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

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

И вот, наконец, свершилось! В ходе выполнения контролируемого кода, непосредственно в try-блоке или в теле одной из вызываемых из этого блока функций возникает ситуация, которая может быть квалифицирована как исключительная. Реакцией на неё является возбуждение с помощью throw-оператора соответствующего исключения. С этого момента весь ход выполнения программы меняется.

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

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

Тем более отменяется выполнение каких-либо операторов вызова. Точка генерации исключения в определённом смысле оказывается действительно точкой. В этой самой точке принципиально меняется весь дальнейший ход выполнения программы. Сразу после возбуждения исключения начинается поиск соответствующего блока перехвата исключения.

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

Существуют чёткие критерии соответствия блока перехвата и возбуждённого исключения. Перечислим их:




    блок перехвата исключения соответствует возбуждённому исключению, если в их объявлении и генерации использован один и тот же тип; если возбуждаемое исключение может быть преобразовано к типу исключения, объявленного в блоке перехвата путём неявного преобразования типа, исключение считается соответствующим данному блоку перехвата; если возбуждаемое исключение преобразуется к типу исключения, объявленного в блоке перехвата путём явного преобразования типа, оно считается соответствующим данному блоку перехвата; исключение, которое является объектом-представителем производного класса, соответствует блоку перехвата, в котором объявлено исключение-представитель базового класса. Таким образом, исключение производного класса может быть перехвачено в блоке перехвата, в котором объявлено исключение-представитель базового класса. Это обстоятельство следует учитывать при расположении в программе блоков, определяющих списки реакций. В списке реакций контролируемого блока операторов перехватчики исключений, порождённых базовыми классами, должны располагаться в списке исключений ниже перехватчиков исключений, представляющих производные классы; блок перехвата, содержащий вместо объявления исключения многоточие catch (...) {/*...*/}, соответствует любому исключению. Это своего рода универсальный блок перехвата. Он должен завершать список перехватчиков, поскольку ни один блок перехвата после него не сможет быть выполнен для обработки данного исключения, поскольку все возможные исключения будут перехвачены этим блоком.


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

Если соответствующий блок перехвата был обнаружен и содержит именованный параметр, временный объект, созданный throw операцией, его инициализирует. Здесь всё происходит примерно также, как и при вызове функции. Для инициализации параметра исключения, являющегося представителем какого-либо класса, может потребоваться собственная версия конструктора копирования и деструктора. Проинициализированный именованный параметр получает доступ к информации, заложенной в исключение в момент его генерации. И здесь уместна аналогия с вызовом функции. Существует проинициализированный и поименованный параметр - будет и доступ к передаваемой информации. В ряде случаев, как и при вызове функции, без конкретного значения параметра можно и обойтись - лишь бы вовремя активизировался соответствующий обработчик и принял бы соответствующие меры по ликвидации последствий исключительной ситуации. А меры в этой связи могут быть приняты самые разнообразные. Здесь всё определяется конкретной задачей.

Стартовав из try-блока, в результате возникновения исключительной ситуации, при благоприятном стечении обстоятельств, мы оказались в одном из связанных с ним блоков перехвата исключения. По сигналу тревоги, благодаря системе программирования C++, в нужное время мы прибыли в нужное место. Теперь всё зависит от программиста. Наши действия в catch-блоке практически ничем не ограничены. Выведем ли мы предупредительное сообщение на экран, исправим ли значение индекса массива, запросим ли новое значение для делителя - это транслятор не волнует. Формально мы совершили действие, в результате которого исключительная ситуация перехвачена, а её причина, возможно, что и ликвидирована. Что бы мы ни сделали catch-блоке (в конце концов, исправляя ошибку, мы можем сделать новую ошибку), будет воспринято без возражений.

Находясь в catch-блоке, мы можем вообще отказаться от каких-либо неотложных мероприятий. С помощью оператора throw; можно повторно возбудить последнее исключение. Этот оператор обязательно должен быть расположен в catch-блоке. В результате повторно запускается всё тот же механизм поиска нового подходящего catch-блока. Стек при этом продолжает разматываться, и если при этом в ходе выполнения программы имела место ситуация "вложенных" контролируемых блоков (из try-блока одной функции прямо или косвенно была вызвана функция, содержащая собственный контролируемый блок), то повторно возбуждённое исключение может быть перехвачено уровнем ниже. Таким образом, можно поручить перехват исключения функции, которая была вызвана ранее и, возможно, не несёт ответственности за возникшую исключительную ситуацию. Если соответствующего перехватчика исключения не окажется, выполнение программы будет остановлено.

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

Может так случиться, что исключение окажется неперехваченным. Не во всех же программах прописывается универсальный блок перехвата… Безуспешный просмотр всех записей стека в поисках соответствующего перехватчика является признаком неперехваченного исключения. Оно оказывается за пределами контролируемого блока операторов, таким же независимым и свободным, как исключение, возбуждённое в "автономном" режиме. И последней преградой на пути неперехваченного исключения встаёт функция unexpected.

Эту функцию невозможно переопределить, а из-за жёстких ограничений на её список параметров (он непременно должен быть пустым), нельзя определить соответствующие совместно используемые функции. Функция unexpected - "вещь в себе", заглушка. Известно лишь, что она вызывает функцию terminate, но может вызвать и ещё какую-либо другую функцию. Изменить ситуацию на этом "последнем рубеже" можно лишь одним единственным способом - определив собственную функцию, которая должна заместить функцию unexpected в результате выполнения уже известной функции set_unexpected. Здесь ещё существует возможность исправить положение. Дальше такой возможности уже не будет.

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

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



#include iostream.h #include string.h #define YESMESS "Мы продолжаем." #define NOMESS "Мы завершаем." class MyDivideByZeroError { char *MyErrorMessage; public: char ContinueKey; MyDivideByZeroError(): MyErrorMessage(NULL) { char YesKey; cout "Зафиксировано деление на нуль." endl; cout "Принимать экстренные меры? (Y/N) "; cin YesKey; if ( YesKey == 'Y' YesKey == 'y' ) { ContinueKey = 1; MyErrorMessage = strdup(YESMESS); } else { ContinueKey = 0; MyErrorMessage = strdup(NOMESS); } } MyDivideByZeroError(const MyDivideByZeroError CopyVal) { ContinueKey = CopyVal.ContinueKey; MyErrorMessage = strdup(CopyVal.MyErrorMessage); } ~MyDivideByZeroError() { if (MyErrorMessage) delete(MyErrorMessage); } void PrintMessage() { cout MyErrorMessage endl; } }; float Dividor(float, float) throw(MyDivideByZeroError); void main() { float MyVal1, MyVal2; for (;;) { // __ Начало контролируемого блока __________________________________. try { cout "========================================" endl; cout "MyVal1 "; cin MyVal1; cout "MyVal2 "; cin MyVal2; cout "Считаем... " Dividor(MyVal1, MyVal2) endl; cout "Получилось! "; } catch (MyDivideByZeroError MyExcept) { MyExcept.PrintMessage(); if (MyExcept.ContinueKey == 0) { cout "Надоело воевать с ошибками! Уходим." endl; break; } } //__ За пределами контролируемого блока ____________________________. cout "Уже за пределами блока. Мы продолжаем..." endl; } } float Dividor(float Val1, float Val2) throw(MyDivideByZeroError) { if (Val2 == 0.0) throw MyDivideByZeroError(); return Val1/Val2; }

И, наконец, пример замещения функций unexpected и terminate. Последняя программа в этой книге.

#include iostream.h #include except.h #define MAXERR 5 class MaxError; class MyError { public: MyError() { CounterError++; if (CounterError MAXERR) { cout " Здесь MyError()... throw MaxError()!" endl; throw MaxError(); } else { cout " Здесь MyError()... CounterError++!" endl; } } void ErrSay() { cout " Здесь ErrSay(): " CounterError endl; } static int CounterError; }; int MyError::CounterError = 0; class MaxError { public: MaxError() { if (CounterMaxError == 0) { /* MaxError один раз может подправить значение счётчика MyError::CounterError. */ CounterMaxError++; MyError::CounterError -= 2; cout "Здесь MaxError().. MyError::CounterError-= 2;" endl; } else { cout " Здесь MaxError()... ###" endl; } } static int CounterMaxError; }; int MaxError::CounterMaxError = 0; void RunnerProcessor(); void Run() throw(MyError); void MyUnex(); void MyTerm(); void main() { unexpected_function OldUnex; terminate_function OldTerm; OldUnex = set_unexpected(MyUnex); OldTerm = set_terminate(MyTerm); /* Мы замещаем функции unexpected() и terminate(). Адресные переменные нужны для того, чтобы запомнить адреса старых функций. В случае необходимости, их можно восстановить: set_unexpected(OldUnex); set_terminate(OldTerm); */ RunnerProcessor(); } void RunnerProcessor() { for (;;) { try { Run(); } catch (MyError err) { err.ErrSay(); } } } void Run() throw(MyError) { cout "Работает Run()..." endl; throw MyError(); } void MyUnex() { /* Мы всё ещё находимся в пределах try-блока. */ cout "Это MyUnex()..." endl; throw MyError(); } void MyTerm() { int MyTermKey = 0; /* Вышли из try-блока. Включилась система автоматического торможения. */ for ( ; MyTermKey 5; ) { cout "Это MyTerm()........................" MyTermKey endl; MyError::CounterError = 0; MaxError::CounterMaxError = 0; RunnerProcessor(); MyTermKey += 1; /* Цикл здесь уже не циклится! */ } MaxError::CounterMaxError = 0; throw MyError(); /* Исключения не работают! */ }

Всё. Приехали. Можно расслабиться. Можно постоять на берегу океана. Послушать шум ветра в соснах. Посмотреть на касаток в холодной прозрачной воде. Только недолго. Впереди ждут великие дела.


Содержание раздела