JavaRush /Курсы /C++ SELF /std::error_code — коды ошибок для I/O, filesystem

std::error_code — коды ошибок для I/O, filesystem

C++ SELF
55 уровень , 0 лекция
Открыта

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, а инструмент для конкретного класса ситуаций.

Вот небольшая таблица:

Инструмент Что выражает Когда подходит лучше всего
std::optional<T>
«значение есть/нет» «не найдено» как нормальная ветка, без причины
expected<T, Error> / variant<T, Error>
«успех или ошибка с причиной» когда вы хотите свою богатую модель ErrorKind + message + context
std::variant<A, B, C...>
«несколько осмысленных исходов» когда исходов больше двух и они все важны
std::error_code
«код ошибки + категория» когда вы в «системной зоне»: 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.

1
Задача
C++ SELF, 55 уровень, 0 лекция
Недоступна
Квитанция терминала
Квитанция терминала
1
Задача
C++ SELF, 55 уровень, 0 лекция
Недоступна
Ярлык причины
Ярлык причины
1
Задача
C++ SELF, 55 уровень, 0 лекция
Недоступна
Парсинг без паники
Парсинг без паники
1
Задача
C++ SELF, 55 уровень, 0 лекция
Недоступна
Виртуальный архив
Виртуальный архив
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ