Перейти к содержимому
Теория · Исключения · урок 4 из 9

Стандартные исключения

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

Например, метод at вектора проверяет индекс и, если элемента нет, бросает исключение std::out_of_range:

C++
#include <iostream>
#include <stdexcept>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3};
    try {
        std::cout << v.at(5) << '\n';
    } catch (const std::out_of_range&) {
        std::cout << "no element 5\n";  // no element 5
    }
}

Программа печатает no element 5: у вектора три элемента, и v.at(5) бросает исключение, которое ловит обработчик.

В теме о контейнерах at для отсутствующего ключа останавливал программу. Так программа завершается при любом исключении, которое никто не поймал.

Иерархия std::exception

Классы стандартных исключений наследуют от std::exception из заголовка <exception>. У std::exception есть виртуальный метод what(): он возвращает текст описания ошибки. Тип у этого текста — const char*, указатель на первый символ, и std::cout печатает его как обычную строку.

Классы из списка ниже объявлены в заголовке <stdexcept>:

  • std::logic_error — ошибка в логике программы: нарушено условие, которое вызывающий код мог проверить заранее;
  • std::invalid_argument (производный от std::logic_error) — недопустимый аргумент;
  • std::out_of_range (тоже производный от std::logic_error) — индекс или ключ вне допустимых границ;
  • std::runtime_error — ошибка, которую можно обнаружить только во время работы программы.

Конструктор каждого из этих классов принимает текст сообщения, и what() возвращает именно его:

C++
int parse_digit(char c) {
    if (c < '0' || c > '9') {
        throw std::invalid_argument("not a digit");
    }
    return c - '0';
}

Обработчик std::exception ловит любое стандартное исключение, а виртуальный what() вызывается у того объекта, который бросили:

C++
try {
    std::cout << parse_digit('x') << '\n';
} catch (const std::exception& e) {
    std::cout << e.what() << '\n';  // not a digit
}

Свой класс исключения тоже объявляют производным от одного из этих классов, обычно от std::runtime_error. Тогда его поймает и обработчик std::exception, а what() вернёт текст, переданный конструктору std::runtime_error.

Бросать значение, ловить ссылку

Выражение throw создаёт из своего операнда отдельный объект — объект исключения. Он живёт по крайней мере до конца обработчика, который его поймал. Поэтому бросают сам объект, а не указатель на него: указатель на локальную переменную станет висячим при раскрутке стека.

Ловят исключение по константной ссылке: const std::exception& e обозначает сам объект исключения, без копии. Поймать можно и по значению, catch (std::exception e), но тогда e — новая копия типа из скобок. Для производного класса это срезка: копия — обычный std::exception, и вызов what() у неё выполняет std::exception::what, а не версию брошенного класса.

Частая ошибка

Исключение создают через new:

C++
try {
    throw new std::runtime_error("disk full");  // ✗
} catch (const std::exception& e) {
    std::cout << e.what() << '\n';
}

Компилятор не предупреждает, но бросили указатель типа std::runtime_error*, а обработчик ждёт ссылку на std::exception. Указатель ему не подходит, другого обработчика нет, и вызывается std::terminate. Бросайте сам объект: throw std::runtime_error("disk full");.

Главное

  • Стандартная библиотека бросает исключения сама: например, at бросает std::out_of_range.
  • Все стандартные исключения производны от std::exception, а what() возвращает текст ошибки.
  • Свой класс исключения объявляют производным от std::runtime_error.
  • Бросают сам объект, а ловят его по константной ссылке: так нет ни висячего указателя, ни срезки.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом