Меню

Какие ошибки называют исключениями

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

Простой пример: в программе-калькуляторе исключением может стать ситуация, когда пользователь решит поделить на ноль. Это не должно стать ошибкой, из-за которой рушится вся программа, но чтобы ситуация не застопорила исполнение остального кода, нужно ее правильно обработать. Для этого необходимы обработчики исключений. Они позволяют «сказать» программе, что ей делать, если такое случится.

Механизм обработки исключений существует в большинстве языков программирования. Он может быть реализован немного по-разному, но общая суть схожа: это всегда какие-то особые случаи, которые надо обработать отдельно. Мы при описании будем отталкиваться от особенностей исключений в Java, но встретить их можно и в других языках: JavaScript, PHP, Python, C++ и так далее.

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

Работа без исключений

Исключения нужны, чтобы программа продолжала относительно корректно работать, даже если что-то пошло не так.

Работа с исключениями

Исключения делятся на две большие группы, которые пересекаются друг с другом: синхронные и асинхронные. Синхронные могут возникнуть только в конкретном месте программы или при выполнении определенной операции: открытие файла, деление и так далее. Асинхронные могут возникнуть когда и где угодно. Их «ловят» по-разному, чтобы успешно отслеживать и те, и другие.

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

  • Разработчик пишет код и понимает, что в какой-то момент в том или ином месте может возникнуть нештатная ситуация. Бывает, что исключения добавляют в уже написанный код — например, нештатную ситуацию обнаружили при тестировании.
  • В этом месте пишется особый блок кода — обработчик. Он говорит программе: здесь может возникнуть особая ситуация, если она случится, выполни вот это.
  • Внутри обработчика — функция, которая выполнится, если программа столкнется с описанной ситуацией. Она или исправит ситуацию, или скорректирует дальнейшее выполнение программы.

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

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

В примере с делением на ноль обработчик может отменить попытку деления и сказать пользователю, что на ноль делить нельзя, — но это самый простой пример. В реальности все сложнее.

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

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

Обработка без возврата — вид обработки, когда проблема не ликвидируется, а участок кода, который не получается выполнить, пропускается. В примере со скриптами обработка «переключила» бы выполнение кода на момент, где уже не понадобится незаработавший скрипт.

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

Обычно асинхронные исключения обрабатывают неструктурно, а синхронные — структурно.

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

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

  • ошибка означает, что программа «упала», что ее работу нельзя продолжить и она должна быть завершена. Ошибку невозможно исправить — только сообщить о ней пользователю, записать в лог и прекратить исполнение кода;
  • исключение — это нештатная ситуация, которую тем не менее можно попробовать починить «на ходу», не закрывая программу. В этом есть смысл, в отличие от ситуации с ошибкой.

Это действительно похожие понятия. В Java, например, сущности исключений и ошибок наследуются от общего предка — интерфейса Throwable. Но ошибка — это явление, когда что-то сделать принципиально не получается. А исключение — ситуация, когда программа просто не знает, что делать, если не указать на это дополнительно.

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

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

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

Обработчики ошибок советуют использовать тогда, когда проблема не решаема изнутри программы. Например, у приложения нет связи с сервером — оно не может продолжать работу без этого. Или какие-то критичные файлы оказались повреждены, и из-за этого код просто нельзя исполнить. Или в системе закончилась свободная память. Это никак не поправить программными способами.

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

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

Большинство наших проектов устроены так: когда во время работы программы возникает какая-то ошибка, то программа аварийно завершается. Иногда при этом она выдаёт сообщение об ошибке. Кажется, что это нормальная ситуация, но на самом деле большинство ошибок можно предусмотреть и научить программу правильно с ними работать. Для этого нам нужны обработчики ошибок. 

Что такое обработчик ошибок

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

  1. Мы заранее прикидываем, в каком месте и почему может возникнуть ошибка.
  2. Пишем в этом месте специальный код, который предупредит компьютер, что это плановая ошибка и что у нас уже есть решение, мол, всё под контролем.
  3. Компьютер применяет наше решение и переходит к следующей команде.
  4. Программа не падает, не завершается с ошибкой, а продолжает работать.

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

Пример программы без обработчика исключений

Допустим, у нас в программе на Python предусмотрено чтение данных из файла и есть такой код:

file = open("myfile2.txt")

Но если на диске этого файла не будет, то компьютер, когда дойдёт до этой строчки, выдаст ошибку:

Что такое обработчик ошибок

Давайте нарисуем это в виде простой схемы:

Что такое исключения в программировании

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

Программа с обработчиком исключений

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

  1. В том месте, где можно предусмотреть ошибку, делают специальный блок.
  2. В этом блоке запускают команду и смотрят, будет ошибка или нет.
  3. Если ошибки нет — программа работает дальше.
  4. Если возникла ошибка — выполнятся то, что написано в обработчике ошибок, а потом программа работает дальше.

В этой ситуации программа не зависнет и не вывалится с ошибкой, а сама сможет её обработать и делать дальше то, что нужно:

Что такое исключения в программировании

try:
    file = open("myfile2.txt")
except FileNotFoundError:
    print("Файл не найден, создаю новый")
    file = open("myfile2.txt","a")

Команда try — это начало нашего обработчика исключений. Она говорит компьютеру: «Попробуй выполнить вот эту команду, а мы посмотрим, что произойдёт». 

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

👉 Сравните текст этой ошибки с тем, что нам выдал компьютер в предыдущем разделе. 

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

Когда что-то не предусмотрено — будет ошибка

Если программе в этом блоке встретится другая ошибка, не та, которую мы предусмотрели, то программа остановится и всё перестанет работать. Например, вот какие ошибки могут возникнуть с файлом:

  • файл есть на диске, но к нему нет прав доступа;
  • файл занят другой программой;
  • сам диск повреждён и данные не читаются.

Во всех этих случаях программа сломается, потому что мы не предусмотрели эти ситуации:

Что такое исключения в программировании

Получается, всё нужно делать с обработкой исключений?

Нет, и вот почему:

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

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

Исключения (исключительные ситуации, exceptions) являются механизмом обработки ошибок, который пришел на смену возврату кода ошибки функциями. Тем не менее, многие разработчики до сих пор используют возврат кода ошибки вместо исключений — в частности очень многие участники конференции C++ Russia 2015 говорили об этом, включая разработчиков Яндекса.

В заметке мы рассмотрим:

  1. обработку каких ошибок можно выполнять с использованием исключений;
  2. синтаксические конструкции, введенные в языки для обработки исключений (примерно одинаковые в разных языках программирования — Java, C++, C# и др.).;
  3. рекомендации по использованию исключений (в т.ч. по книгам о чистоте кода);

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

Что следует понимать под ошибкой?

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

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

Способы обработки ошибок

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

  1. возврат кода ошибки. Например, если при отправке сообщения по сети выяснится, что соединение разорвано, функция отправки может вернуть единицу, а при успешной отправке — ноль;
  2. вернуть заведомо некорректный результат. Например, функция malloc, выделяющая память в языке Си при ошибке (невозможности выделить память) возвращает ноль, а в остальных случаях — начальный адрес выделенного фрагмента;
  3. вернуть любое допустимое значение и выставить глобальный флаг ошибки (в языке С++ для этого может использоваться глобальная переменная errno.
  4. аварийно завершить работу (в С++ для этого используются функции abort или exit);
  5. выработать исключение (подробнее написано ниже);

Конечно, помимо завершения работы, вы всегда можете записать описание сложившейся ситуации в log-файл (поток cerr в С++) или вывести на экран.
Аварийное завершение работы — это худший способ обработки ошибки, т.к. программист фактически расписывается в своей неспособности что-то исправить.
С точки зрения последовательности выполнения команд, возврат кода ошибки ничем не отличается от возврата некорректного значения или выставления флага ошибки в глобальный объект. Во всех этих случаях функция, которая не знает как корректно обработать входные данные передает эти обязанности непосредственно коду, который ее вызвал. Чаще всего это работает хорошо, однако:

  1. если вызывающий код не обрабатывает код ошибки (он ведь не обязан это делать), то он продолжает вычисления, но уже с некорректными данными. Такая ситуация выявится не сразу, ведь вы можете получить некорректный результат или аварийный останов совсем с другом месте. Например, в библиотеке Qt есть функция преобразования строки в целое число:
    int QString::toInt(bool * ok = 0, int base = 10) const
    В случае ошибки она вернет ноль и присвоит значение false аргументу ok (который вообще не является обязательным). Если наш код вызывает эту функцию для строки, не являющейся целым число — то он продолжит выполнение, получив в качестве результата ноль (вполне корректное, но неверное значение);
  2. далеко не всегда удается правильно обработать ошибку непосредственно в коде, вызвавшем функцию. В результате получается примерно следующая ситуация:
    bool foo(QString str) {
      bool ok;
      string.toInt(&ok);
      if (false == ok) {
        return false;
      }
      // ... some actions
    }
    bool bar() {
      QString str;
      // ... some actions
      if (false == foo(str)) 
        return fasle;
      }
    }

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

В этом контексте, исключения можно рассматривать как механизм доставки информации об ошибки до точки программы, где эту ошибку наиболее естественно обрабатывать. Доставка ошибки при этом будет выполняться точно также, как и серия операторов return в приведенном выше фрагменте кода — каждая функция получившая исключение передает управление «наверх» (раскручивая стек), процесс продолжается до тех пор, пока исключение не попадет в блок trt{}, содержащий обработчик, совместимый с типом исключения.
Исключением также называют объект, некоторого класса, являющийся представлением ошибки (исключительного случая).

Синтаксические конструкции механизма обработки исключений

Фрагмент кода, внутри которого могут вырабатываться исключения должен быть помещен в блок try{}, к которому могут быть добавлены обработчики — блоки catch(ExceptionType) {}. Функция может вырабатывать исключения при помощи оператора throw. Например:

try {
  socket = connect_to_server(host); // throw BadHostException or ServerNotAvailableException
  send_message(socket, message); // throw ServerNotAvailableException or BadMsgException
}
catch(BadHostException exception) {
  // BadHostException hadler
  // can charge other host from user
}
catch(ServerNotAvailableException exception) {
  // ServerNotAvailableException handler
  // can reconnect for example
}
catch(BadMsgException exception) {
  // BadMsgException hadler
  // can notify user by window
}
catch(...) {
  // other types of exceptions hadler
}

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

Исключения могут организовываться в иерархии посредством наследования. Например, в стандартной библиотеке C++ определен базовый класс std::exception, от которого наследуется std::logic_error (класс логических ошибок), являющий базовым для std::out_of_range. Для фрагмента кода, рассмотренного выше, мог быть выделен класс NetworkException, базовый для BadHostException и ServerNotAvailableException. При этом, если при обработке нам не важно какие именно проблемы с сетью произошли — мы можем написать обработчик для базового класса.

К блоку try{} может быть написано несколько обработчиков, совместимых с одним и тем же типом исключения. Блоки catch обрабатываются в порядке их перечисления, поэтому будет выбран тот, который описан выше. Например:

catch(BadHostException exception) {
  // BadHostException hadler
}
catch(NetworkException exception) {
  // all network exceptions, but not BadHostException
}
catch(ServerNotAvailableException exception) {
  // never call, because ServerNotAvailableException was hanled as NetworkException
}

Рекомендации по использованию исключений

Выше описаны общие (подходящие для большинства языков) принципы обработки исключений. Однако, в каждом языке есть свои тонкости. Так, например, в языке Java есть ключевое слово finally, задающее фрагмент кода, который будет выполнен при возникновении исключения любого вида. В языке C++, например: надо учитывать то, что при обработке throw создается копия исключения; в обработчике исключения запись throw; позволяет передать текущее исключение дальше без копирования; и множество других тонкостей [3]. Во многих языках можно задать спецификацию исключений для функции (используйте спецификации исключений), т.е. список исключений, которые могут выходить из нее — если вдруг какой-либо участок кода начнет вырабатывать новый тип исключения — вы получите ошибку на этапе компиляции. Изучите особенности реализации механизма исключений для своего языка программирования.

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

Роберт Мартин рекомендует начинать написание функции с try-catch-finally. Это заставит программиста еще раз задумать о том, какие ошибки могут произойти — на самом деле, почти любая функция может столкнуться с ситуациями, в которых не сможет продолжить вычисления [2].

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

Пишите код, безопасный с точки зрения исключений (exception-safe)

Что такое «код, безопасный с точки зрения исключений»? Пусть имеется какой-то произвольный фрагмент кода. Где-то в процессе выполнения этого кода генерируется исключение и перехватывается снаружи. Этот фрагмент кода безопасен с точки зрения исключений если он, в идеале, отвечает следующим требованиям:

  1. Все ресурсы, выделенные внутри блока try до генерации исключения, должны быть корректно освобождены;
  2. Все объекты, созданные внутри блока try до генерации исключения, должны быть корректно уничтожены;
  3. Должен произойти полный откат всех изменений в системе, внесенных кодом, который выполнился от начала блока try до момента генерации исключения.

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

Транзакционность и «бизнес-логика»

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

Рассмотрим в качестве примера графический редактор, имеющий multi-dialog интерфейс, и следующий сценарий работы: пользователь выбирает у главном меню «Open file(s)», выделяет десять картинок и нажимает «Open». Восемь картинок загружаются корректно, загрузка девятой генерирует исключение.

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

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

Обеспечение транзакционности

Некоторые авторитетные люди, чьи книги издаются чуть ли не миллионными тиражами, утверждают, что одна из причин, по которой исключения лучше чем коды ошибок, заключается в том, что код, занимающийся ремонтом программы можно отделить от логики работы программы, поместив его в блоки catch. К сожалению, этот подход устарел уже на следующий день после выхода первого Стандарта языка C++, а его использование говорит о полном непонимании всей прелести языка.

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

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

Типы данных

Практически во всех ситуациях наиболее естественный тип данных для пользовательских исключений с точки зрения C++ это тип, так или иначе производный от std::exception. Моя рекомендация состоит в том, чтобы в качестве типа данных для пользовательских исключений использовать std::runtime_error, или его наследников, если это необходимо.

Некоторые люди (не буду показывать пальцем) используют оператор new для генерации исключений и перехватывают их по указателю. Такая конструкция в свое время была (а может быть и до сих пор есть) в макросах Visual Assist-а. Такой подход в C++ в корне неверен — применять его можно только в языках со сборщиком мусора (gc). Использование такого подхода в C++ говорит о полном непонимании механизма исключений. Генерировать исключения следует по значению, перехватывать — по константной ссылке.

Классификация и оповещение

Любая ошибка имеет причину и влечет следствие. Используйте виртуальную функцию std::exception::what() для того, чтобы получить человеческое описание причины возникновения ошибки.

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

namespace network
{

class error : public std::runtime_error
{
    virtual char const* what() const;
};

} // namespace network

namespace ui
{

class error : public std::runtime_error
{
    virtual char const* what() const;
};

} // namespace ui

int main()
{
    application app;

    while(app.alive())
    {
        try
        {
            app.run();
        }
        catch(network::error const& e)
        {
            std::cout << "Network exception: " << e.what() << std::endl;
            mailto("admin@microsoft.com", e.what());
        }
        catch(ui::error const& e)
        {
            std::cout << "UI exception: " << e.what() << std::endl;
            message_box(e.what());
        }
        catch(std::exception const& e)
        {
            std::cout << "Exception: " << e.what() << std::endl;
        }
    }
    return 0;
}

Следствие ошибки напрямую зависит от того места, где исключение было перехвачено. Следствие любого перехваченного исключения в коде, безопасном с точки зрения исключений, это откат системы на блок try, в котором это исключение было сгенерированно. То есть, говоря по-простому, следствие это «что не получилось сделать», в то время как причина это «почему это не получилось сделать». Место генерации исключения и его тип — это причина, место его перехвата — это следствие.

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

Не могу загрузить изображение (следствие):
недостаточно прав для чтения файла 001.jpg (причина)

Подведем итоги

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

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

Тут же, можно вспомнить, что исключения не используются во многих проектах по причинам недостаточной эффективности — например в Яндекс.Браузере. Кроме того, исключения не использовались в библиотеке Qt (по крайней мере 4.х версии) из-за сложности их реализации на некоторых архитектурах. Тот факт, что в настоящее время библиотека Qt сейчас использует механизм исключений означает, что его поддерживает подавляющее большинство компиляторов С++ под самые разные платформы.

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

Пишите код, транзакционный с точки зрения исключений. Реализуйте транзакционность на уровне описания типов данных, максимально минимизируя ремонт программы в блоках catch. Сопоставляйте блокам try соответствующие транзакции с точки зрения «бизнес-логики». Используйте std::runtime_error или его наследников в качестве типов данных для исключений. Генерируйте исключения по значению, перехватывайте по константной ссылке. Классифицируйте ошибки, если это необходимо. Предоставляйте понятное человеку сообщение об ошибке; указывайте как причину, так и следствие. Оповещайте об исключении способом, не генерирующим исключения.

Литература по теме обработки исключений:

  1. Б. Страуструп Язык программирования С++. Специальное издание. Пер. с англ. – М.: Издательство Бином, 2011 г. – 1136 с.
  2. Мартин Р. Чистый код. Создание, анализ и рефакторинг. Библиотека программиста. – СПб.: Питер, 2014. – 464 с.
  3. Мейерс С. Эффективное использование С++. 35 новых рекомендаций по улучшению ваших программ и проектов. – М.: ДМК Пресс, 2014. – 294с.

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Какие ошибки нельзя допускать при применении пузыря со льдом
  • Какие ошибки наблюдения вы знаете