1. Введение
Когда вы только учитесь программировать, кажется, что ошибки бывают двух видов: «я забыл точку с запятой» и «всё остальное». Но чем ближе вы к реальному миру (ввод, файлы, права доступа, сеть, ОС), тем больше ошибок становятся ожидаемыми: файл может отсутствовать, каталог может быть недоступен, имя может быть занято. std::error_code — это способ говорить про такие сбои спокойно, как про данные, без паники и без исключений.
Если очень по-человечески: std::error_code — это «квитанция об ошибке». Пустая квитанция означает успех. Непустая — содержит «код» и «категорию», чтобы можно было понять, что пошло не так, и принять решение.
Важно, что в стандартной библиотеке есть операции, которые умеют не бросать исключения, а вместо этого записывать информацию об ошибке в std::error_code& (например, перегрузки в std::filesystem — мы ещё будем подробно изучать файловую систему позже, но сам факт таких перегрузок вам полезно знать уже сейчас).
Ментальная модель: «ошибка как значение», но стандартизированная
Если вы читали код новичков, то знаете классический стиль: «верну -1, а дальше разберётесь». Проблема тут в том, что -1 внезапно может быть и нормальным значением, а «разберётесь» обычно превращается в «никто не разобрался».
std::error_code предлагает стандартную модель:
- успех выражается пустым std::error_code (по умолчанию);
- ошибка выражается непустым std::error_code (проверяется как if (ec)).
И это отлично ложится на нашу философию дня: «ошибка — это данные».
Небольшая схема того, как обычно выглядит такой контракт:
flowchart TD
A["Функция выполняет операцию"] --> B{"Успех?"}
B -->|да| C["Возвращаем значение или пишем out-параметр"]
B -->|нет| D["Возвращаем std::error_code (непустой)"]
D --> E["Вызывающий решает: повторить, сообщить, завершить"]
Важный нюанс: std::error_code особенно уместен там, где ошибка — часть нормального контроля потока. Например: «попробуй открыть», «если не открылось — это не крах вселенной, просто сообщи и продолжай».
2. Минимальный интерфейс std::error_code: что нужно запомнить
Сейчас будет хорошая новость: вам не нужно знать std::error_code на уровне «все методы и категории». Достаточно очень маленького набора, чтобы писать аккуратный и понятный код.
Подключается он так:
#include <system_error>
А пользоваться им чаще всего так:
- std::error_code ec; — «пока всё хорошо».
- if (ec) — «появилась ошибка».
- ec.message() — человекочитаемое сообщение (для вывода пользователю).
- ec.value() — числовой код (обычно для диагностики/логов, но не для «магической логики»).
- сравнение с другими std::error_code (например, с теми, что получены из std::errc).
Вот самый маленький пример «почувствовать руками»:
#include <iostream>
#include <system_error>
int main() {
std::error_code ec; // пустой = успех
std::cout << "ok? " << std::boolalpha << !ec << '\n'; // ok? true
// Имитируем ошибку: создадим error_code из стандартной причины
ec = std::make_error_code(std::errc::invalid_argument);
if (ec) {
std::cout << "error: " << ec.message() << '\n'; // текст зависит от ОС/локали
std::cout << "value: " << ec.value() << '\n';
}
}
Обратите внимание на одну важную вещь: текст message() может отличаться между системами. Поэтому message() — это удобно печатать, но очень не хочется использовать как основу логики («если message == "..." то…»). Логику лучше строить на сравнениях кодов/причин.
4. std::errc: переносимые причины ошибок
Чтобы вы не сравнивали ec.value() с загадочными числами вроде 2, 13 или 12345 (а потом не искали по ночам, что это было), стандарт даёт перечисление std::errc. Это набор типовых причин в стиле «нет такого файла», «доступ запрещён», «неверный аргумент».
Прелесть std::errc в том, что:
- вы пишете код, который читается по смыслу;
- стандартная библиотека умеет превращать errc в error_code.
Ключевой инструмент здесь — std::make_error_code.
Пример проверки «это ошибка неверного аргумента?»:
#include <system_error>
bool is_invalid_input(const std::error_code& ec) {
return ec == std::make_error_code(std::errc::invalid_argument);
}
Здесь логика устойчивее, чем «сравнить ec.value() с 22» (вдруг у вас другая ОС, другая библиотека, другая платформа).
5. Типичный стиль без исключений: out-параметр + std::error_code
Сейчас мы соберём «самый стандартный» стиль функции, которая не хочет бросать исключения, но хочет сообщить об ошибке. Обычно это выглядит так: функция возвращает std::error_code, а результат кладёт в переменную, переданную по ссылке.
Да, это слегка «олдскульно», но у такого подхода есть огромный плюс: он очень хорошо сочетается со многими API стандартной библиотеки, где ошибка — ожидаемый исход.
Добавляем в учебное приложение аккуратный парсер числа
Представим, что мы развиваем консольное приложение (условно назовём его TaskCLI), которое принимает команды от пользователя. В командах часто есть числа: id задачи, приоритет, лимит. Нам нужен парсер «без исключений» и без optional, потому что причину ошибки иногда полезно знать хотя бы на уровне «неверный ввод».
Напишем parse_uint_ec: если удалось — запишем в out, если нет — вернём error_code.
#include <string_view>
#include <system_error>
std::error_code parse_uint_ec(std::string_view text, unsigned& out) {
if (text.empty()) {
return std::make_error_code(std::errc::invalid_argument);
}
unsigned value = 0;
for (char c : text) {
if (c < '0' || c > '9') {
return std::make_error_code(std::errc::invalid_argument);
}
value = value * 10u + static_cast<unsigned>(c - '0');
}
out = value;
return {}; // пустой error_code = успех
}
Здесь важно, что return {} — это «всё хорошо». Не true, не 0, не -1. Просто пустой std::error_code.
Использование рядом с вызовом
Теперь вызываем:
#include <iostream>
#include <string>
#include <system_error>
std::error_code parse_uint_ec(std::string_view text, unsigned& out);
int main() {
std::string s;
std::cin >> s;
unsigned id = 0;
std::error_code ec = parse_uint_ec(s, id);
if (ec) {
std::cerr << "Invalid id: " << ec.message() << '\n';
return 1;
}
std::cout << "Parsed id = " << id << '\n';
}
Заметьте хороший стиль: мы проверяем ec сразу, рядом с вызовом. Это как проверять, закрыли ли вы дверь, выходя из квартиры, а не через три недели на море.
6. Как применять std::error_code в проекте: контекст, печать, пример
Где std::error_code встречается в стандартной библиотеке
Важно понимать контекст: std::error_code — это не «какой-то наш учебный велосипед». Это один из официальных способов, которыми стандартная библиотека умеет возвращать информацию об ошибках без исключений.
Часто это выглядит как «перегрузка с дополнительным параметром std::error_code&». Например, в файловых операциях (модуль про std::filesystem будет позже) вы можете встретить функции, которые принимают error_code и вместо исключения записывают туда ошибку.
Чтобы увидеть идею (не углубляясь в файловую систему), можно представить типичный паттерн так:
// Псевдо-форма: "сделай, а ошибку запиши в ec"
operation(args..., std::error_code& ec);
Где ec после вызова:
- пустой → успех;
- непустой → ошибка, и вы решаете, что делать.
И вот тут std::errc снова полезен: вы можете сравнить «причину» с переносимыми значениями, а не строить логику на тексте ошибки.
Связка с нашей моделью ошибок: как не устроить «зоопарк контрактов»
Очень легко устроить «зоопарк контрактов», когда одна функция возвращает optional, другая variant, третья error_code, а четвёртая вообще печатает в cout и продолжает как ни в чём не бывало.
Чтобы такого не случилось, держите простую мысль: std::error_code — это не конкурент expected, а инструмент для конкретного класса ситуаций.
Вот небольшая таблица:
| Инструмент | Что выражает | Когда подходит лучше всего |
|---|---|---|
|
«значение есть/нет» | «не найдено» как нормальная ветка, без причины |
|
«успех или ошибка с причиной» | когда вы хотите свою богатую модель ErrorKind + message + context |
|
«несколько осмысленных исходов» | когда исходов больше двух и они все важны |
|
«код ошибки + категория» | когда вы в «системной зоне»: I/O, стандартная библиотека/ОС, ожидаемые сбои |
То есть в нашем TaskCLI логика бизнес-ошибок может жить в Error (как в предыдущих лекциях), а там, где мы общаемся с «внешним миром» (потоки, файлы, окружение), — часто естественнее принимать/возвращать std::error_code и уже потом переводить его в наш формат.
Как красиво печатать error_code
Новички часто делают так: «если ошибка — распечатаю всё, что знаю, в трёх местах». Потом получают три одинаковых сообщения и одно противоречивое, и остаётся только философски смотреть в окно.
Базовый «вежливый» подход: печать должна быть короткой, а место печати — предсказуемым (обычно рядом с границей программы: CLI-обработчик команды, main, верхний уровень сценария). Сегодня мы не строим полноценную политику, но маленькая аккуратная функция печати уже полезна:
#include <iostream>
#include <system_error>
void print_ec(const std::error_code& ec) {
if (!ec) {
std::cout << "OK\n";
return;
}
std::cerr << "ERROR: " << ec.message() << '\n';
}
Тут важно не переборщить: message() — достаточно, чтобы человек понял, что случилось. value() — можно добавить для диагностики, но не как смысловую основу.
Мини-связка в приложении: разбор команды delete <id> без исключений
Соберём небольшой кусочек проекта, чтобы было ощущение, что мы не в вакууме, а развиваем приложение.
Представим, что команда удаления задачи выглядит как строка, а id мы уже научились парсить через parse_uint_ec. Если парсинг не удался — сообщаем. Если удался — пытаемся удалить (само удаление мы тут упрощаем до заглушки, потому что сейчас нас интересует именно обработка ошибки парсинга).
#include <iostream>
#include <string_view>
#include <system_error>
std::error_code parse_uint_ec(std::string_view text, unsigned& out);
std::error_code handle_delete(std::string_view id_text) {
unsigned id = 0;
std::error_code ec = parse_uint_ec(id_text, id);
if (ec) return ec;
// Допустим, дальше будет поиск задачи по id и удаление.
// Здесь мы показываем только слой ввода.
return {};
}
int main() {
// Пример: пользователь ввёл "abc" вместо числа
std::error_code ec = handle_delete("abc");
if (ec) {
std::cerr << "delete failed: " << ec.message() << '\n';
return 1;
}
std::cout << "deleted\n";
}
Что важно в этой мини-связке: handle_delete не печатает внутри себя. Он возвращает статус, а печать происходит на верхнем уровне. Это уменьшает шанс «дублирования реплик» от вашей программы.
7. Типичные ошибки при работе с std::error_code
Ошибка №1: сравнивать ec.value() с «магическими числами».
Такое часто начинается с невинного «ну у меня тут 2 означает “нет файла”», а заканчивается тем, что код работает только на одной машине и только по вторникам. Лучше сравнивать с std::errc через std::make_error_code, потому что это выражает смысл и меньше привязано к платформе.
Ошибка №2: использовать ec.message() как основу логики.
message() нужен, чтобы показать человеку текст, но этот текст зависит от системы, локали и реализации. Если вы начнёте писать «если message содержит "denied"», ваш код быстро превратится в гадание на кофейной гуще. Для логики используйте сравнение с кодами/причинами, а текст оставляйте для вывода.
Ошибка №3: забыть проверить error_code и продолжить как будто всё хорошо.
std::error_code не заставляет вас проверять себя (в отличие от компилятора, который заставляет ставить ;). Поэтому дисциплина здесь ручная: получил ec — проверь рядом. Чем дальше проверка от вызова, тем больше шансов, что вы уже не помните, что именно могло пойти не так.
Ошибка №4: печатать ошибку и одновременно возвращать её наверх.
Это почти гарантирует дублирование сообщений, особенно когда проект растёт. Внутренний слой пусть возвращает error_code, а внешний слой решает, что печатать и где. Иначе получится эффект «программа нервничает и повторяет одно и то же».
Ошибка №5: пытаться лечить всё std::error_code, даже там, где нужна богатая причина.
std::error_code хорош для системных причин («неверный аргумент», «нет доступа»), но он не заменяет вашу модель ошибок уровня предметной области. Если вам важно различать «задача не найдена» и «задача уже выполнена» — это не error_code, это ваш ErrorKind/variant/expected.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ