Возвращаемое значение как признак ошибки
Простейший способ сообщения об ошибках предполагает использование возвращаемого значения функции или метода. Функция сохранения объекта в базе данных может возвращать логическое значение: true в случае успешного сохранения, false – в случае ошибки.
class Database{public: bool SaveObject(const Object&obj);};Соответственно, вызов метода должен выглядеть так:if (database.SaveObject(my_obj) == false ){ //обработка ошибки}Обработка ошибки, разумеется, зависит от конкретной программы. Типична ситуация, когда при многократно вложенных вызовах функций обработка происходит на несколько уровней выше, чем уровень, где ошибка произошла. В таком случае результат, сигнализирующий об ошибке, придется передавать во всех вложенных вызовах.
int main(){ if (fun1()==false ) //обработка ошибки return 1;}boolfun1(){ if (fun2()==false ) return false ; return true ;}boolfun2(){ if (database.SaveObject(obj)==false ) return false ; return true ;}Если функция или метод должны возвращать какую-то величину в качестве результата, то особое, недопустимое, значение этой величины используется в качестве признака ошибки. Если метод возвращает указатель, выдача нулевого указателя применяется в качестве признака ошибки. Если функция вычисляет положительное число, возврат - 1 можно использовать в качестве признака ошибки.
Иногда невозможно вернуть признак ошибки в качестве возвращаемого значения. Примером является конструктор объекта, который не может вернуть значение. Как же сообщить о том, что во время инициализации объекта что-то было не так?
Распространенным решением является дополнительный атрибут объекта – флаг, отражающий состояние объекта. Предположим, конструктор класса Database должен соединиться с сервером базы данных.
class Database{public : Database(const char *serverName); ... bool Ok(void )const {return okFlag;};private : bool okFlag;};Database::Database(const char*serverName){ if (connect(serverName)==true ) okFlag =true ; else okFlag =false ;}int main(){ Database database("db-server"); if (!database.Ok()){ cerr <<"Ошибка соединения с базой данных"<<endl; return 0; } return 1;}Лучше вместо метода Ok, возвращающего значение флага okFlag, переопределить операцию ! (отрицание).
class Database{public : bool operator !()const {return !okFlag;};};Тогда проверка успешности соединения с базой данных будет выглядеть так:
if (!database){ cerr <<"Ошибка соединения с базой данных"<<endl;}Следует отметить, что лучше избегать такого построения классов, при котором возможны ошибки в конструкторе. Из конструктора можно выделить соединение с сервером базы данных в отдельный метод Open :
и тогда отпадает необходимость в операции ! или методе Ok().
Использование возвращаемого значения в качестве признака ошибки – метод почти универсальный. Он применяется, прежде всего, для обработки запланированных ошибочных ситуаций. Этот метод имеет ряд недостатков. Во-первых, приходится передавать признак ошибки через вложенные вызовы функций. Во-вторых, возникают неудобства, если метод или функция уже возвращают значение, и приходится либо модифицировать интерфейс, либо придумывать специальное "ошибочное" значение. В-третьих, логика программы оказывается запутанной из-за сплошных условных операторов if с проверкой на ошибочное значение.
Исключительные ситуации
В языке Си++ реализован специальный механизм для сообщения об ошибках – механизм исключительных ситуаций. Название, конечно же, наводит на мысль, что данный механизм предназначен, прежде всего, для оповещения об исключительных ситуациях, о которых мы говорили чуть ранее. Однако механизм исключительных ситуаций может применяться и для обработки плановых ошибок.
Исключительная ситуация возникает при выполнении оператора throw . В качестве аргумента throw задается любое значение. Это может быть значение одного из встроенных типов (число, строка символов и т.п.) или объект любого определенного в программе класса.
При возникновении исключительной ситуации выполнение текущей функции или метода немедленно прекращается, созданные к этому моменту автоматические переменные уничтожаются, и управление передается в точку, откуда была вызвана текущая функция или метод. В точке возврата создается та же самая исключительная ситуация, прекращается выполнение текущей функции или метода, уничтожаются автоматические переменные, и управление передается в точку, откуда была вызвана эта функция или метод. Происходит своего рода откат всех вызовов до тех пор, пока не завершится функция main и, соответственно, вся программа.
Предположим, из main была вызвана функция foo , которая вызвала метод Open , а он в свою очередь возбудил исключительную ситуацию:
class Database{public : void Open(const char*serverName);};voidDatabase::Open(const char*serverName){ if (connect(serverName)==false ) throw 2;}foo(){ Database database; database.Open("db-server"); String y; ...}int main(){ String x; foo(); return 1;}В этом случае управление вернется в функцию foo , будет вызван деструктор объекта database , управление вернется в main , где будет вызван деструктор объекта x , и выполнение программы завершится. Таким образом, исключительные ситуации позволяют аварийно завершать программы с некоторыми возможностями очистки переменных.
В таком виде оператор throw используется для действительно исключительных ситуаций, которые практически никак не обрабатываются. Гораздо чаще даже исключительные ситуации требуется обрабатывать.