Урок №188. Недостатки и опасности использования исключений

  Юрий  | 

    | 

  Обновл. 13 Июл 2019  | 

 2269

 ǀ   2 

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

Очистка памяти

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

Что произойдёт, если writeFile() не сработает и выбросит объект класса-исключения FileException? К этому моменту мы уже открыли файл, и точка выполнения перейдёт к обработчику catch, который выведет ошибку и завершит своё выполнение. Обратите внимание, операция закрытия файла closeFile(filename) никогда не выполнится! Этот код должен быть переписан следующим образом:

Такой тип ошибок часто возникает и в другой форме при работе с динамическим выделением памяти:

Если processPerson() выбросит исключение, то точка выполнения перейдёт к обработчику catch. В результате память, выделенная alex-у, никогда не освободится! Этот пример немного сложнее, чем предыдущий, поскольку alex находится внутри блока try, и выходит из области видимости при завершении выполнения блока try. Это означает, что обработчик исключений вообще не может получить доступ к alex-у (этот объект к моменту выполнения блока catch уже будет уничтожен), поэтому возможности освободить память у нас нет.

Однако есть два относительно простых способа это исправить. Во-первых, нужно объявить alex вне блока try, чтобы он не уничтожался при завершении выполнения блока try:

Поскольку alex объявлен вне блока try, то он доступен как в блоке try, так и в обработчике catch. Это означает, что обработчик catch сможет выполнить очистку памяти должным образом.

Второй способ — локально использовать объект класса, который умеет самостоятельно выполнять после себя очистку памяти при выходе из области видимости (это ещё называют «умным указателем»). Стандартная библиотека C++ предоставляет класс std::unique_ptr, который может использоваться в этих целях. std::unique_ptr — это шаблон класса, который содержит указатель и освобождает выделенную ему память при выходе из области видимости.

Мы поговорим больше об умных указателях в следующей главе.

Исключения и деструкторы


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

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

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

Проблемы с производительностью

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

Примечание: Некоторые современные компьютерные архитектуры поддерживают модель исключений «Исключения с нулевой стоимостью» (zero-cost исключения). Исключения с нулевой стоимостью, если они поддерживаются, не требуют дополнительных затрат при выполнении программы в случае отсутствия ошибок (именно в этом случае мы больше всего заботимся о производительности). Тем не менее, исключения с нулевой стоимостью потребляют ещё больше ресурсов в случае обнаружения исключения.

В каких случаях стоит использовать исключения?


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

   Обрабатываемая ошибка возникает редко.

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

   Ошибка не может быть обработана в том месте, где она возникает.

   Нет хорошего альтернативного способа возвратить код ошибки обратно в caller.

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

Давайте рассмотрим:

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

   Функция также не может обработать эту ошибку: повторно запрашивать пользователя ввести (новое) имя файла — это не то, что должна выполнять эта функция и это может быть даже неуместным (в зависимости от логики реализации вашей программы).

   И четвёртое условие: есть ли хороший альтернативный способ возвратить код ошибки обратно в caller? Это зависит от деталей реализации вашей программы. Если такой способ есть (например, вы можете вернуть нулевой указатель или код возврата, указывающий на ошибку), это, вероятно, лучшее решение. Если нет, то лучшим решением будет использовать исключение и его дальнейшую обработку.

Учитывая всё вышесказанное вы должны научиться понимать и отличать риски и пользу от использования (и обработки) исключений.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (29 оценок, среднее: 4,72 из 5)
Загрузка...

Комментариев: 2

  1. Аватар Алексей:

    Про директиву noexcept ещё можно пару слов сказать

  2. Аватар kmish:

    В статье:
    "Этот пример немного сложнее, чем предыдущий, поскольку alex находится внутри блока try, и выходит из области видимости при завершении выполнения блока try. Это означает, что обработчик исключений вообще не может получить доступ к alex-у (этот объект к моменту выполнения блока catch уже будет уничтожен), поэтому возможности освободить память у нас нет."
    Это не так. alex будет жить до окончания блока catch (в комментариях к предыдущему уроку я это показал), почти наверняка до того блока catch в стеке при раскручивании, который обработает исключение, но это нужно проверять. Почему так, не знаю. Возможно, try и catch параллельно существуют, т.к. между ними даже пустую строку нельзя поставить — синтаксис ругается сразу. Но реализовано это не так как у функций. То что catch способен принимать неконстантные ссылки, наверняка, тоже из серии иной реализации всего механизмов исключений.

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *