C++. Бархатный путь. Часть 2 - Реакция на исключительную ситуацию

ОГЛАВЛЕНИЕ

 

Реакция на исключительную ситуацию 

Реакция на исключительную ситуацию называется исключением.

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

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

#define MAX 10 int PushIntArray(int* const, int, int); void main() { int intArray[MAX]; int IndexForArray, ValueForArray; ::::: for (;;) { ::::: // Значения IndexForArray и ValueForArray меняются в цикле. if (!PushIntArray(intArray, IndexForArray, ValueForArray)) { cout << "Некорректное значение индекса" << endl; IndexForArray = 0; } ::::: } ::::: } int PushIntArray(int* const keyArray, int index, int keyVal) { if (index >= 0 && index < MAX) { keyArray[index] = keyVal;// Спрятали значение и сообщили об успехе. return 1; } else return 0; // Сообщили о неудаче. }

Перед нами самый простой вариант исключения как формы противодействия синхронной исключительной ситуации. Из функции main вызывается функция, PushIntArray, которой в качестве параметров передаются адрес массива, значение индекса и значение, предназначенное для сохранения в массиве.

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

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

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

#include <iostream.h> #define EXDIVERROR 0.0 /* Здесь может быть определено любое значение. Это не меняет сути дела. Так кодируется значение, предупреждающее об ошибке. Не самая хорошая идея: некоторые корректные значения всегда будут восприниматься как уведомления об ошибке. */ float exDiv (float, float); void main() { float val1, val2; ::::: if (exDiv(val1, val2) == EXDIVERROR) { ::::: cout << "exDiv error…"; // Здесь можно попытаться исправить ситуацию. ::::: } } float exDiv (float keyVal1, float keyVal2) { if (val2) return keyVal1/keyVal2; return EXDIDERROR; }

Функция exDiv может быть модифицирована следующим образом: возвращаемое целочисленное значение сообщает о ходе вычисления, а непосредственно сам результат вычисления передаётся по ссылке.

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

#include <iostream.h> int exDiv (float, float, float&); void main() { float val1, val2, resDiv; ::::: if (!exDiv(val1, val2, resDiv)) { ::::: cout << "exDiv error…"; ::::: } } int exDiv (float keyVal1, float keyVal2, float& keyRes) { if (val2) {keyRes = keyVal1/keyVal2; return 1;} return 0; }

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

#include <iostream.h> class DivAnsver { public: int res; float fValue; // Конструктор. DivAnsver(): res(1), fValue(0.0) {}; // ctorИнициализаторы в действии! }; DivAnsver exDiv (float, float); void main() { DivAnsver Ansver; Ansver = exDiv(0.025, 0.10); cout << Ansver.fValue << "..." << Ansver.res << endl; Ansver = exDiv(0.025, 0.0); cout << Ansver.fValue << "..." << Ansver.res << endl; } DivAnsver exDiv (float val1, float val2) { DivAnsver Ans; if (val2) Ans.fValue = val1/val2; else Ans.res = 0; return Ans; }

Функция exDiv возвращает значение объекта Ans (предопределённый конструктор копирования об этом позаботится). При этом, если деление возможно, значение данного-члена res оказывается равным единице, а fValue принимает значение частного от деления. В противном случае res устанавливается в нуль и объект Ans становится исключением.

Подобным изменениям можно подвергнуть объявление класса, реализующего стек: возвращаемое функцией pop() значение объекта-представителя шаблонного класса мог бы содержать результат выполнения функции и значение содержимого стека. 

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

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

Казалось бы, всё хорошо и на этом можно было бы остановиться. Однако, нет пределов совершенству!

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

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

С ростом числа вариантов возвращаемых значений становится всё более актуальной проблема разделения "положительных" и "отрицательных" ответов.

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

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