Мы уже ранее говорили о механизмах обработки ошибок в языке С++, таких как cerr(), exit() и assert(). Однако мы не успели поговорить о еще одной очень важной теме — «Исключения в языке С++». Сейчас мы это исправим.
Когда коды возврата не работают
При написании повторно используемого кода возникает необходимость в обработке ошибок. Одним из наиболее распространенных способов обработки потенциальных ошибок является использование кодов возврата (или «кодов завершения»), которые возвращает оператор return. Например:
1 2 3 4 5 6 7 8 9 10 11 |
int findFirstChar(const char* string, char ch) { // Перебираем каждый символ строки for (int index=0; index < strlen(string); ++index) // Если текущий символ совпадает со значением переменной ch, то возвращаем индекс этого символа if (string[index] == ch) return index; // Если совпадение не найдено, то возвращаем -1 return -1; } |
Эта функция возвращает индекс первого символа передаваемой строки, совпадающего со значением переменной ch
. Если символ не найден, то функция возвращает -1
в качестве индикатора ошибки.
Главным преимуществом этого подхода является его простота. Однако есть ряд недостатков, которые могут быстро проявиться в нетривиальных случаях.
Во-первых, возвращаемые значения не всегда понятны. Если функция возвращает -1
, обозначает ли это какую-то специфическую ошибку или это корректное возвращаемое значение? Часто это бывает трудно понять, не видя перед глазами код самой функции.
Во-вторых, функции могут возвращать только одно значение. А что, если нам нужно будет возвратить как результат выполнения функции, так и код завершения? Например:
1 2 3 4 |
double divide(int a, int b) { return static_cast<double>(a)/b; } |
Здесь нужен механизм обработки ошибок, потому что, если пользователь передаст 0
в качестве параметра b
, произойдет сбой. Кроме того, функция также должна возвратить и результат выполнения операции static_cast<double>(a)/b
. Как же это сделать? Один из вариантов — возврат результата операции или кода завершения по ссылке, например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> double divide(int a, int b, bool &success) { if (b == 0) { success = false; return 0.0; } success = true; return static_cast<double>(a)/b; } int main() { bool success; double result = divide(7, 4, success); // мы сейчас передаем значение типа bool, чтобы знать заранее, будет ли операция успешной if (!success) // проверяем результат выполнения операции перед фактическим использованием result std::cerr << "An error occurred" << std::endl; else std::cout << "The answer is " << result << '\n'; } |
В-третьих, когда кода много, то многие вещи могут пойти не так, как нужно, поэтому коды возврата нужно постоянно проверять. Рассмотрим следующий фрагмент программы, в котором проводится анализ текстового файла на наличие определенных значений:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
std::ifstream setupIni("setup.ini"); // открываем setup.ini для чтения // Если файл нельзя открыть (например, потому что он отсутствует), то возвращаем ошибку if (!setupIni) return ERROR_OPENING_FILE; // Если же файл можно открыть, то считываем значения из этого файла if (!readIntegerFromFile(setupIni, m_firstParameter)) // пытаемся найти значение типа int в файле return ERROR_READING_VALUE; // возвращаем ошибку, если значение не найдено if (!readDoubleFromFile(setupIni, m_secondParameter)) // пытаемся найти значение типа double в файле return ERROR_READING_VALUE; if (!readFloatFromFile(setupIni, m_thirdParameter)) // пытаемся найти значение типа float в файле return ERROR_READING_VALUE; |
Мы еще не рассматривали работу с файлами, поэтому не волнуйтесь, если вы не понимаете, как и что здесь работает — просто обратите внимание на то, что для каждого вызова функции требуется проверка и возврат состояния обратно в caller. Теперь представьте, если бы у нас было двадцать параметров разных типов — нам бы пришлось выполнять проверку и возврат ERROR_READING_VALUE
двадцать раз! Весь этот механизм обработки ошибок только затрудняет понимание (чтение) того, что же на самом деле должна делать эта функция.
В-четвертых, коды возврата не очень хорошо сочетаются с конструкторами. Что произойдет, если мы создадим объект, а внутри конструктора случится что-то катастрофическое? Конструкторы не могут использовать оператор return для возврата индикатора состояния, а передача по ссылке может причинить массу неудобств, и её нужно явно проверять. Кроме того, даже если мы это сделаем, объект все равно создастся, и лечить мы уже будем последствия (либо обрабатывать, либо удалять).
Наконец, при возврате ошибки обратно в caller, сам caller может не всегда быть готовым обработать эту ошибку. Если caller не хочет обрабатывать ошибку, он либо игнорирует её (что уже плохо), либо возвращает ошибку обратно в функцию, от которой он её и получил. Это не то что неудобно, это может привести к сбою программы или к неопределенным результатам.
Основная проблема с кодами возврата заключается в том, что они плотно связаны с общим потоком выполнения кода, а это, в свою очередь, ограничивает наши возможности.
Исключения
Обработка исключений как раз и обеспечивает механизм, позволяющий отделить обработку ошибок или других исключительных обстоятельств от общего потока выполнения кода. Это предоставляет больше свободы в конкретных ситуациях, уменьшая при этом беспорядок, который вызывают коды возврата.
На следующем уроке мы рассмотрим принципы обработки исключений в языке C++.
все хорошо и понятно, но пример "мимо кассы"
выражение:
ни при каких обстоятельствах не приведет к сбою. Мало того, она выдает хорошо обрабатываемые результаты в случае b == 0:
inf, при a != 0
nan, при a == 0
Проблема не в примере, он очень даже кстати. На ноль делить нельзя — это факт. Пример основывался на том, что на ноль делить нельзя и компилятор, из-за этого выдаёт ошибку(даже любой калькулятор выдаёт ошибку).
вот этим и отличается компилятор от калькулятора. как вуз от школы.
в школе на ноль делить нельзя. а в вузе можно.
и компилятор даже вполне себе ответ возвращает.