JavaRush /Курси /C++ SELF /std::error_code — коди помилок для I/O та файлової систем...

std::error_code — коди помилок для I/O та файлової системи

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 << "успіх? " << std::boolalpha << !ec << '\n';  // успіх? true

    // Імітуємо помилку: створимо error_code зі стандартної причини
    ec = std::make_error_code(std::errc::invalid_argument);

    if (ec) {
        std::cout << "помилка: " << ec.message() << '\n';    // текст залежить від ОС/локалі
        std::cout << "код: " << ec.value() << '\n';
    }
}

Зверніть увагу на важливу деталь: текст message() може відрізнятися залежно від системи. Тому message() зручно виводити, але його зовсім не хочеться використовувати як основу логіки («якщо message == "...", то…»). Логіку краще будувати на порівнянні кодів і причин.

3. 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» — раптом у вас інша ОС, інша бібліотека чи інша платформа.

4. Типовий стиль без винятків: 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 << "Некоректний id: " << ec.message() << '\n';
        return 1;
    }

    std::cout << "Прочитаний id = " << id << '\n';
}

Зауважте, який це вдалий стиль: ми перевіряємо ec одразу, поруч із викликом. Це як перевірити, чи ви зачинили двері, коли виходили з квартири, а не згадати про це за три тижні у відпустці.

5. Як застосовувати 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 << "Успіх\n";
        return;
    }
    std::cerr << "Помилка: " << 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 << "Не вдалося видалити: " << ec.message() << '\n';
        return 1;
    }
    std::cout << "Видалено\n";
}

Що важливо в цьому невеликому прикладі: handle_delete не друкує нічого всередині себе. Він повертає статус, а виведення відбувається на верхньому рівні. Це зменшує ймовірність «дублювання реплік» від вашого застосунку.

6. Типові помилки під час роботи з 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.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ