Урок №185. Классы-Исключения и Наследование

  Юрий Ворон  | 

    | 

  Обновл. 14 Дек 2018  | 

 440

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

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

Теперь, если пользователь передаст недопустимый index, программа выдаст ошибку. Хотя это сообщит пользователю, что что-то пошло не так, лучшим вариантом было бы сообщить «по-тихому» caller-у, что что-то пошло не так и пусть он с этим разберется соответствующим образом (как именно — мы пропишем позднее).

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

Теперь, если пользователь передаст недопустимый index, operator[] сгенерирует исключение типа int.

Когда конструкторы терпят неудачу

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

Классы-исключения


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

В этом примере, если мы поймаем исключение типа int, что оно нам сообщит? Был ли передаваемый index недопустим? Может operator+ вызвал целочисленное переполнение или может оператор new не сработал из-за нехватки памяти? Хотя мы можем генерировать исключения типа const char*, которые будут указывать ПРИЧИНУ сбоя, это все еще не даст нам возможности обрабатывать исключения из разных источников по-разному.

Одним из способов решения этой проблемы является использование классов-исключений. Класс-исключение — это обычный класс, который выбрасывается в качестве исключения. Создадим простой класс-исключение, который будет использоваться с нашим ArrayInt:

Вот полная программа:

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

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

Исключения и Наследование

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

Здесь выбрасывается исключение типа Child. Однако результат выполнения этой программы:

caught Parent

Что случилось?

Во-первых, как мы уже говорили, дочерние классы могут быть пойманы обработчиком родительского класса. Поскольку Child является дочерним классу Parent, то из этого следует, что Child «является» Parent («является» — тип отношения). Во-вторых, когда C++ пытается найти обработчик для выброшенного исключения, он делает это последовательно. Первое, что он проверяет — это, подходит ли обработчик исключений класса Parent для исключений класса Child. Поскольку Child «является» Parent, то блок catch для объектов класса Parent подходит и, соответственно, выполняется! В этом случае блок catch для объектов класса Child никогда не выполняется.

Чтобы этот пример работал по-другому, нам нужно изменить порядок последовательности блоков catch:

Результат:

caught Child

Таким образом, обработчик Child будет ловить и обрабатывать исключения класса Child. Исключения класса Parent не соответствуют обработчику Child (Child «является» Parent, но Parent «не является» Child) и, таким образом, будут обрабатываться только обработчиком Parent.

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

std::exception


Многие классы и операторы из стандартной библиотеки С++ выбрасывают классы-исключения при сбое. Например, оператор new и std::string могут выбрасывать std::bad_alloc при нехватке памяти. Неудачное динамическое приведение типов с помощью dynamic_cast выбрасывает исключение std::bad_cast и т.д. Начиная с C++14, существует 21 классы-исключения, которые могут быть выброшены, в C++17 их еще больше.

Хорошей новостью является то, что все эти классы-исключения являются дочерними классу std::exception. std::exception – это небольшой интерфейсный класс, который используется в качестве родительского класса для любого исключения, которое выбрасывается в стандартной библиотеке C++.

В большинстве случаев, если исключение выбрасывается стандартной библиотекой С++, нам все равно, будет ли это неудачное выделение, конвертирование или что-либо другое. Нам достаточно знать, что произошло что-то катастрофическое, из-за чего в нашей программе произошел сбой. Благодаря std::exception мы можем настроить обработчик исключений типа std::exception, который будет ловить и обрабатывать как std::exception, так и все (21+) дочерние ему классы-исключения!

Результат выполнения программы выше:

Standard exception: string too long

В этом примере всё довольно просто. В std::exception есть виртуальный метод what(), который возвращает строку C-style с описанием исключения. Большинство дочерних классов переопределяют функцию what(), изменяя это сообщение. Обратите внимание, эта строка C-style предназначена для использования только в качестве описания.

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

В этом примере исключения типа std::bad_alloc перехватываются и обрабатываются первым обработчиком. Исключения типа std::exception и всех других дочерних ему классов-исключений обрабатываются вторым обработчиком.

Такие иерархии наследования позволяют использовать определенные обработчики для перехвата определенного типа исключений или для перехвата одним (родительским) обработчиком всей иерархии исключений.

Использование стандартных исключений напрямую

Ничто не генерирует std::exception напрямую, и вы также должны придерживаться этого правила. Однако вы можете генерировать исключения других классов из стандартной библиотеки С++, если они адекватно отражают ваши потребности. Найти список всех стандартных классов-исключений из стандартной библиотеки С++ вы можете здесь.

std::runtime_error (находится в заголовочном файле stdexcept) является популярным выбором, так как имеет общее имя, а конструктор принимает настраиваемое сообщение:

Результат:

Standard exception: Bad things happened

Создание собственных классов-исключений дочерних классу std::exception


Конечно, вы можете создать свои собственные классы-исключения дочерние классу std::exception и переопределить виртуальный константный метод what(). Вот та же программа, что и выше, но с классом-исключением ArrayException дочерним std::exception:

В C++11 к виртуальной функции what() добавили спецификатор noexcept (который означает, что функция обещает не выбрасывать исключения самостоятельно). Следовательно, в C++11 и более поздних версиях наше переопределение метода what() также должно иметь спецификатор noexcept.

Вам решать, хотите ли вы создавать свои собственные классы-исключения, использовать классы-исключения из стандартной библиотеки С++ или писать классы-исключения дочерние std::exception. Всё зависит от ваших целей.

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

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

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

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