1. Зачем нужен std::exception, если можно бросать что угодно
Если смотреть на C++ строго формально, то бросать можно почти что угодно: число, строку, структуру, кота (если кот копируемый). Проблема в том, что ловить “что угодно” неудобно: вы не знаете, как это печатать, как классифицировать и как писать общий обработчик.
std::exception решает именно эту бытовую боль: он даёт единый минимальный интерфейс “я ошибка, вот мой текст”.
std::exception — это базовый класс стандартных исключений. Главное, что он приносит в наш код — метод:
const char* what() const noexcept;
То есть исключение (если оно совместимо со std::exception) умеет сказать вам текстовое описание проблемы. Не идеальное, не всегда подробное, но хотя бы что-то, а не “ой, всё”.
Важно и практично следующее: подавляющее большинство исключений стандартной библиотеки наследуются от std::exception, поэтому “универсальный обработчик” обычно выглядит как catch (const std::exception& e).
Небольшая схема, чтобы закрепить роль std::exception:
flowchart TD
A["throw (какой-то объект ошибки)"] --> B{Ищем подходящий catch}
B -->|"catch (const SomeType&)"| C[Специфичная обработка]
B -->|"catch (const std::exception& e)"| D["Общий обработчик
можно распечатать e.what()"]
B -->|"catch (...)"| E["План Б
тип неизвестен, what() нет"]
2. Базовые правила: как ловить исключения
Ловим std::exception по const&
Когда исключение уже вылетело из глубины ваших функций и прилетело в catch, вы обычно хотите две вещи: во‑первых, не потерять информацию об ошибке, во‑вторых, не устроить лишние копирования.
Поэтому “золотая формула” для общего случая звучит так: ловим по const ссылке. Это общепринятая практика: полиморфные исключения ловят по ссылке, чтобы не терять тип и поведение.
Вот минимальный скелет “поймали и распечатали”:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("something went wrong");
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n'; // error: something went wrong
}
}
Обратите внимание: мы пока не обсуждаем “какой тип исключения выбирать” — это следующая лекция. Здесь нам важен именно общий интерфейс: если исключение — наследник std::exception, то у него есть what().
Почему const? Потому что в обработчике вы обычно не должны “чинить” исключение. Вы должны его прочитать, вывести сообщение, возможно — завершить операцию. А & (ссылка) — чтобы не делать копию и не потерять динамический тип (об этом очень скоро).
Порядок catch: где место std::exception, а где — catch (...)
Когда в коде несколько обработчиков, компилятор выбирает первый подходящий по типу. Поэтому порядок catch — это не “как мне красиво”, а часть логики.
На практике std::exception — это “широкая сеть”: она ловит очень многое. Если поставить её первой, все более специфичные обработчики ниже станут недостижимыми.
Здесь важно удержать в голове правильную лестницу:
- Сначала — самые конкретные типы (если вы их обрабатываете по‑особенному).
- Потом — общий обработчик const std::exception&.
- И только потом — catch (...), как последний рубеж, когда тип вообще неизвестен.
Мини‑пример, чисто чтобы увидеть структуру (без углубления в типы — оно будет дальше по плану):
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("boom");
} catch (const std::runtime_error& e) {
std::cout << "runtime_error: " << e.what() << '\n'; // runtime_error: boom
} catch (const std::exception& e) {
std::cout << "std::exception: " << e.what() << '\n';
} catch (...) {
std::cout << "unknown error\n";
}
}
Обратите внимание: catch (...) не даёт вам what(), потому что там нет объекта исключения определённого типа. Это “ловим всё, но ничего не знаем”. Поэтому catch (...) — это не замена нормальной диагностики, а страховочная сетка, чтобы программа не падала молча.
3. what(): что возвращает и как печатать
Когда вы впервые видите what(), мозг новичка часто ожидает std::string: мол, “ну текст же”. Но исторически (и по соображениям совместимости) what() возвращает const char*. Это значит: у вас в руках указатель на C‑строку, то есть строку, заканчивающуюся '\0'.
Самый простой (и правильный) сценарий — просто вывести what() прямо в catch:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("division by zero");
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // division by zero
}
}
Почему это удобно? Потому что вы получаете хотя бы одну строку в логах/консоли вместо загадочного “программа завершилась”.
Немного про время жизни
Здесь есть тонкость: what() возвращает указатель на память, которая принадлежит объекту исключения. Пока вы внутри catch, объект исключения живой, и строка обычно валидна. Но как только вы вышли из catch, хранить этот указатель “на потом” — плохая идея.
Плохой пример (делать так не нужно):
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
const char* msg = nullptr;
try {
throw std::runtime_error("bad input");
} catch (const std::exception& e) {
msg = e.what();
}
std::cout << msg << '\n'; // ⚠ может быть уже невалидно (висячий указатель)
}
Хороший, “человеческий” вариант — скопировать текст в std::string, если вам нужно использовать сообщение после catch:
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>
int main() {
std::string msg;
try {
throw std::runtime_error("bad input");
} catch (const std::exception& e) {
msg = e.what(); // копируем текст
}
std::cout << msg << '\n'; // bad input
}
Запомните простую метафору: what() — это “записка”, приклеенная к объекту исключения. Пока объект у вас в руках — записку можно читать. Если хотите унести записку домой — перепишите в свой блокнот (std::string).
4. Почему нельзя ловить std::exception по значению
Сейчас будет момент, который ломает мозг ровно один раз — и потом экономит вам часы отладки.
Исключения часто полиморфные: вы бросаете объект производного типа, например “какой-то конкретный runtime error”, а ловите — по базовому типу std::exception. Это нормально, это задумано.
Но если вы ловите по значению:
catch (std::exception e) { ... }
то происходит срезание объекта (object slicing): производная часть “отрезается”, и в переменной e остаётся только базовая часть std::exception. А значит, вы можете потерять “правильную” реализацию what() и получить максимально бесполезный текст.
Давайте сравним лоб в лоб.
Плохой вариант — ловим по значению:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("file not found");
} catch (std::exception e) { // ⚠ ловим по значению
std::cout << e.what() << '\n'; // часто будет что-то вроде "std::exception"
}
}
Хороший вариант — ловим по ссылке:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("file not found");
} catch (const std::exception& e) { // ✅ по ссылке
std::cout << e.what() << '\n'; // file not found
}
}
Вот почему правило “ловим по const&” — не эстетика и не религия, а способ не потерять сообщение об ошибке.
Можно думать так: ловля по значению — это как если бы вам принесли коробку с надписью “Хрупкое стекло”, а вы перепаковали её в коробку “Просто коробка” и удивились, что на ней нет инструкции “не ронять”.
5. Практический пример: мини‑калькулятор и диагностика через what()
Сейчас соберём небольшой консольный “калькулятор для бедных, но гордых”. Он будет читать строки вида 12 / 3 и печатать результат. А если что-то пошло не так — вместо молчаливой смерти он выведет сообщение ошибки через what().
Шаг 1: функция деления с проверкой на ноль
Начнём с маленькой функции. Если делить на ноль, мы не хотим возвращать “магическое значение” типа 0 и делать вид, что всё нормально. Мы хотим сигнализировать ошибку.
#include <stdexcept>
double safe_div(double a, double b) {
if (b == 0.0) {
throw std::runtime_error("division by zero");
}
return a / b;
}
Да, здесь используется std::runtime_error. Сегодня нам важно не “почему именно он”, а то, что его можно поймать как std::exception и прочитать what().
Шаг 2: вычисление выражения по оператору
Добавим маленький диспетчер операций. Ничего умного, просто четыре действия.
#include <stdexcept>
double eval(double a, char op, double b) {
if (op == '+') return a + b;
if (op == '-') return a - b;
if (op == '*') return a * b;
if (op == '/') return safe_div(a, b);
throw std::runtime_error("unknown operator");
}
Обратите внимание на стиль: если оператор неизвестен, мы не пытаемся “как‑то угадать”. Мы прекращаем операцию через исключение.
Шаг 3: парсинг строки через std::stringstream
Теперь читаем строку и достаём a op b. Мы делали потоковый парсинг раньше, поэтому тут ничего магического.
#include <sstream>
#include <stdexcept>
#include <string>
struct Expr {
double a{};
char op{};
double b{};
};
Expr parse_expr(const std::string& line) {
std::stringstream ss(line);
Expr e{};
if (!(ss >> e.a >> e.op >> e.b)) {
throw std::runtime_error("bad expression format");
}
return e;
}
Если строка пустая или кривая, мы бросаем исключение. А кто будет решать, что с этим делать? Тот, кто управляет сценарием ввода-вывода: обычно это main() или функция уровня “run”.
Шаг 4: граница обработки — try/catch в main()
Теперь соберём “тонкий” main, который умеет: читать строку, парсить, считать, печатать. И, что важно, в одном месте ловить ошибки как std::exception и печатать what().
#include <exception>
#include <iostream>
#include <string>
int main() {
try {
std::string line;
std::getline(std::cin, line);
const Expr ex = parse_expr(line);
const double result = eval(ex.a, ex.op, ex.b);
std::cout << result << '\n'; // например: 4
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n';
}
}
Что мы выиграли? Если ввод был 12 / 0, программа не падает молча и не печатает “0”. Она честно скажет error: division by zero. Если ввод был hello, скажет error: bad expression format.
А главное — этот стиль масштабируется: вы можете углублять программу, добавлять функции, и всё равно иметь общий обработчик, который не превращает отладку в гадание на кофейной гуще.
Границы ответственности
Заметьте, как распределились роли. Функции parse_expr и eval не печатают ошибки и не спрашивают пользователя “а вы точно это имели в виду?”. Они просто сообщают: “я не могу сделать работу”. А main() — это тот, кто решает, что делать с этой новостью: показать сообщение и завершиться (или в более сложном приложении — попросить ввести заново).
Это хороший дизайн уже на уровне новичка: меньше каши из std::cout по всему проекту.
6. Типичные ошибки при работе с std::exception и what()
Когда вы начинаете использовать исключения в реальном коде, ошибки чаще всего не в “сложной теории”, а в мелких привычках. Многие из них выглядят безобидно, потому что программа иногда даже “работает”, но диагностика становится плохой, а поведение — странным. Сейчас разберём самые частые грабли, чтобы вы наступили на них максимум один раз (и то в учебных целях).
Ошибка №1: ловить std::exception по значению (catch (std::exception e)).
Это приводит к object slicing: вы теряете динамический тип исключения, а вместе с ним — полезное сообщение what() и иногда другие детали. В результате в консоли появляется что-то вроде “std::exception” вместо нормального текста. Правильная привычка: catch (const std::exception& e).
Ошибка №2: использовать только catch (...) и думать, что этого достаточно.
catch (...) действительно ловит “всё”, но он не даёт вам what(), потому что у вас нет типизированного объекта исключения. Итог: вы ловите ошибку, но не понимаете какую. Нормальный минимум — иметь обработчик catch (const std::exception& e) и выводить e.what(), а catch (...) оставлять как последний рубеж.
Ошибка №3: сохранять const char* из what() и использовать после выхода из catch.
Внутри catch это обычно работает, а после — может внезапно превратиться в “мусор” или привести к неопределённому поведению. Если вам нужно сообщение позже, копируйте: std::string msg = e.what();. Это простая операция, которая экономит кучу нервов.
Ошибка №4: делать пустой catch и “глотать” ошибку.
Иногда новичок пишет catch (...) {} или catch (const std::exception&) {} “чтобы не падало”, и программа продолжает жить в поломанном состоянии. Это похоже на заклеивание лампочки Check Engine чёрной изолентой: на панели стало красиво, а внутри всё так же плохо. Если вы ловите исключение, у вас должна быть стратегия: вывести сообщение, прекратить операцию, вернуть управление выше.
Ошибка №5: ставить catch (const std::exception& e) выше более конкретных обработчиков.
Если общий обработчик стоит раньше, до специфичных catch управление никогда не дойдёт. Это не только “неаккуратно”, но и может сломать вашу логику обработки ошибок. Держите порядок: конкретные типы выше, std::exception ниже, catch (...) — последним.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ