Menu

Исключения в C++: throw, what() и обработка ошибок

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Зачем нужны исключения

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

Исключения решают эту проблему. Когда что-то идёт не так, вы выбрасываете объект с помощью throw. Выполнение немедленно останавливается, стек разворачивается (для каждого локального объекта между выбросом и обработчиком вызывается деструктор), и управление переходит к ближайшему подходящему catch. Необработанное исключение нельзя тихо проигнорировать: если его ничто не перехватит, программа вызовет std::terminate и аварийно завершится.

Эта страница посвящена стороне выброса — самим объектам ошибок. Следующая страница подробно разбирает механику try/catch.

Выброс и сообщение what()

Технически вы можете выбросить (throw) любое значение —throw 42; или throw "oops"; допустимы— но не делайте этого. Соглашение, которому следуют все, — выбрасывать объект, производный от std::exception. Этот базовый класс объявляет единственный виртуальный метод what(), возвращающий описание проблемы в виде const char*. Соблюдение соглашения означает, что один-единственный catch (const std::exception& e) может обработать что угодно.

Заголовок <stdexcept> даёт вам готовые типы, конструктор которых принимает сообщение:

Обратите внимание, что what() возвращает ровно ту строку, с которой вы сконструировали исключение. Заметьте также, что мы перехватили его по const exception&, хотя выбросили runtime_error: это работает, потому что runtime_error является std::exception (отношение, знакомое вам по странице о наследовании).

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

Прежде чем писать собственный тип исключения, проверьте, нет ли уже подходящего в стандартной библиотеке. Все они наследуются от std::exception и в <stdexcept> делятся на два семейства:

  • logic_error — ошибка в логике программы, которую в принципе можно было бы выявить ещё до запуска. К подтипам относятся invalid_argument, out_of_range, domain_error и length_error.
  • runtime_error — сбой, проявляющийся только во время выполнения и сам по себе не являющийся ошибкой программирования. К подтипам относятся range_error, overflow_error и underflow_error.

Многие функции библиотеки выбрасывают их за вас. Например, std::vector::at() проверяет границы и выбрасывает out_of_range вместо того, чтобы позволить вам прочитать за концом:

Этот at() — безопасный аналог v[9]. Простой operator[] не проверяет границы: чтение v[9] здесь — это неопределённое поведение, а не исключение. Выбор at() — это способ превратить тихое повреждение данных в перехватываемую ошибку.

Выбирайте тип, который описывает ошибку: invalid_argument, когда вызывающий передаёт нечто бессмысленное, out_of_range для проблем с индексом/ключом, runtime_error для случаев «внешний мир меня подвёл».

Создание собственного типа исключения

Когда ни один стандартный тип не подходит — вы хотите прикрепить дополнительные данные или перехватить (catch) именно вашу ошибку и ничего больше — определите класс, наследующий от std::exception (или от одного из его подтипов), и переопределите what(). Наследование от std::runtime_error — самый простой путь, потому что он уже хранит сообщение и реализует what() за вас:

Поскольку NetworkError несёт код состояния, обработчик может на него реагировать: повторить попытку при 5xx, сдаться при 4xx. Голая строка с ошибкой так не смогла бы. Пользовательский тип также позволяет catch (const NetworkError&) перехватывать только сетевые проблемы, оставляя всё остальное более общему обработчику ниже.

Если вы когда-нибудь будете наследоваться напрямую от std::exception (а не от runtime_error), не забудьте переопределить what() самостоятельно и пометить его noexcept, чтобы он совпадал с сигнатурой базового класса:

class ParseError : public std::exception {
public:
    const char* what() const noexcept override {
        return "failed to parse input";
    }
};

Выбрасывайте по значению, перехватывайте по ссылке

Это самое важное правило исключений в C++, и именно его новички нарушают. Выбрасывайте объекты по значению и перехватывайте их по const-ссылке.

throw runtime_error("oops");            // по значению - правильно
catch (const runtime_error& e) { ... }  // по const-ссылке - правильно

Перехват по значению вместо этого —catch (std::exception e)— копирует исключение в объект базового класса и срезает производную часть. После срезки e.what() вызывает реализацию базового класса, а не вашу переопределённую, поэтому ваше тщательно составленное сообщение исчезает:

try {
    throw NetworkError(503, "service unavailable");
} catch (std::exception e) {       // по значению - срезка объекта!
    std::cout << e.what();         // обобщённое сообщение, status() пропал
}

Ссылка (&) сохраняет настоящий динамический тип, поэтому виртуальный what() диспетчеризуется корректно, и вы по-прежнему можете обращаться к производным членам. Добавьте const, потому что вы только читаете исключение, а не изменяете его. Никогда не выбрасывайте указатель (throw new runtime_error(...)): перехватившему пришлось бы вызвать для него delete — но на каком пути выполнения? Это ровно та утечка, которую исключения и призваны предотвращать.

Далее: try-catch

Теперь вы умеете создавать и выбрасывать (throw) корректные исключения и выбирать подходящий стандартный тип для каждого сбоя. Вторая половина истории — сторона перехвата. Следующая страница полностью разбирает try/catch: упорядочивание нескольких блоков catch от самого конкретного к самому общему, перехватывающий всё catch (...), повторный выброс с помощью голого throw; и то, как RAII (вспомните умные указатели) гарантирует освобождение ваших ресурсов по мере разворачивания стека.

Часто задаваемые вопросы

Что такое исключение в C++?

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

В чём разница между throw и return для ошибок?

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

Что делает метод what() в исключениях C++?

Каждый класс, производный от std::exception, предоставляет виртуальный метод what(), возвращающий const char* с описанием ошибки. Когда вы перехватываете исключение, вызов e.what() даёт вам понятное человеку сообщение, которое можно записать в лог или вывести. Стандартные типы исключений формируют его из строки, переданной в их конструктор.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ