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

  Юрий  | 

  |

  Обновл. 15 Сен 2021  | 

 47337

 ǀ   7 

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

Исключения в перегрузке операторов

Рассмотрим следующую перегрузку оператора индексации [] для простого целочисленного класса-массива:

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

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

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

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

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


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

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

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

В этом примере, если мы поймаем исключение типа int, что оно нам сообщит? Был ли передаваемый index недопустим? Может оператор + вызвал целочисленное переполнение или может оператор 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, существует больше 20 классов-исключений, которые могут быть выброшены, а в C++17 их еще больше.

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

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

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

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 (176 оценок, среднее: 4,88 из 5)
Загрузка...

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

  1. Павел:

    В контейнерах STL out of range выбрасывает assert, который не ловится исключениями.
    А так хотелось 🙁 Нужно видимо проходить отладкой.

  2. koh:

    переопределить виртуальный константный метод what().

    а разве не нужно добавлять virtual к const char* what()… ? виртуальная ведь функция…

    1. vb:

      virtual имеет смысл только в родительском классе, у дочерних классов добавляют для удобства, чтобы не забыть. Еще в дочерних классах вместо virtual используют override, что позволяет опознать перегруженную функцию.

      1. Анатолий:

        Не совсем так:
        «Модификатор override может использоваться с любым методом, который должен быть переопределением. Достаточно просто указать override в том месте, где обычно указывается const (после скобок с параметрами). Если метод не переопределяет виртуальную функцию родительского класса, то компилятор выдаст ошибку.»

  3. kmish:

    В статье написано:

    можно еще писать:

  4. kmish:

    Уф… провел следующее исследование: Код:

    ArrayException("Invalid index") является анонимным объектом и по идее (из предыдущих уроков данного туториала) имеет область видимости данной строки.
    Но! Смотрим далее:

    Что мы видим? Мы ломим НЕконстантную ссылку на анонимный объект! Как такое может быть???
    Дописав наш пример и выполнив следующий код мы получим:

    Результат:

    ArrayException construct:1
    An array exception occurred (Invalid index)
    ArrayException destruct:1
    End of main

    Что происходит?

    1. Создается анонимный объект throw ArrayException("Invalid index", 1) — вызывается конструктор ArrayException;
    2. сatch принимает неконстантную ссылку на объект;
    3. Выполняется сatch;
    4. Вызывается деструктор.

    Из чего делается вывод:
    Область видимости ArrayException("Invalid index", 1) почему-то выходит за пределы выражения, даже без объявления константной ссылки. Скорее всего под капотом C++ происходят какие-то совсем сложные вещи.

    1. Илья:

      Исключения не относятся к стандартному потоку кода, поэтому не удаляются при развёртывании стека и могут быть переданы по НЕконстантной ссылке (посмотри диспетчер задач или мониторинг системы Visual Studio, там явно написано, что любая твоя прога запускает два потока).

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

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