C++. Бархатный путь. Часть 2 - Работа системы управления исключением
ОГЛАВЛЕНИЕ
Работа системы управления исключением
Генерацию и перехват исключений не рекомендуется использовать в целях, отличных от обработки ошибок. Считается, что это может уменьшить ясность программы.
Считается также, что механизмы обработки исключением изначально создавались для обработки сравнительно редко проявляющихся ошибок и использовались чаще всего для завершения работы программы. В силу этого нет (пока нет) никакой гарантии относительно оптимальности, эффективности и надёжности этого механизма в качестве средства для обычного программного управления.
Вместе с тем, далеко не каждая исключительная ситуация должна вести к завершению программы. Например, при вычислении частного от деления двух случайных чисел, система управления исключением в случае возможного деления на нуль оказывается одним из основных средств управления программой.
Примерно такая же ситуация складывается и в нашем примере. Мы специально моделируем исключительные ситуации для оценки возможностей применения механизма перехвата.
Мы не будем всякий раз прерывать ход выполнения программы из-за того, что возникла какая-то странная ситуация. Если мы в силах восстановить нормальный ход выполнения программы - мы должны сделать это.
И если исключительная ситуация возникает в цикле - пусть её перехватчик остановит цикл. А вопросы эффективности и корректной работы со стеком - это вопросы к транслятору.
#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. Здесь ещё существует возможность исправить положение. Дальше такой возможности уже не будет.