JavaRush /Курсы /C++ SELF /error_code‑overload

error_code‑overload

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

1. Два стиля API в std::filesystem

Когда вы пишете код, который работает с диском, вы внезапно начинаете жить в мире, где внешняя реальность может в любой момент сказать: «нет». Файл могли удалить, директорию могли переименовать, права доступа могли запретить чтение — и это не всегда «катастрофа», иногда это штатная ветка логики. Если на каждую такую ситуацию бросать исключение и ловить его, код становится нервным, шумным и похожим на сериал, где каждый эпизод заканчивается драматической музыкой.

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

Бросающие функции и error_code‑перегрузки

В std::filesystem у многих операций есть два стиля API.

Первый — «бросающий»: если что-то пошло не так, функция кидает исключение (обычно std::filesystem::filesystem_error).

Второй — «не бросающий»: функция принимает ссылку на std::error_code, записывает туда ошибку и не кидает исключение. Такой стиль как раз и нужен, когда ошибка ожидаема по сценарию. Идея перегрузок с error_code — нормальная часть дизайна filesystem.

Выглядит это обычно так: есть функция fs::exists(p) (бросающая форма) и рядом fs::exists(p, ec) (форма «ошибка как значение»). То же касается многих других операций.

Смысл простой: вы выбираете стиль обработки ошибок под вашу задачу. Если ошибка — неожиданная (например, «не смог создать важную рабочую директорию приложения»), исключения могут быть уместны. Если ошибка — ожидаемая (например, «временного файла могло не быть»), то error_code‑перегрузка чаще удобнее.

Мини‑анатомия std::error_code

std::error_code — это небольшой объект, который внутри хранит «код ошибки» и «категорию» (упрощённо: откуда эта ошибка и как её интерпретировать). В повседневном коде нам важны три вещи: можно ли проверить «ошибка есть?», можно ли её обнулить, и можно ли получить человекочитаемое сообщение. Именно для этого обычно используют: if (ec), ec.clear() и ec.message().

Сразу важный психологический момент: std::error_code — это не исключение, он никого не «роняет». Он просто лежит рядом и говорит: «если ты спросишь, я расскажу, что пошло не так». В этом есть плюс: вы сами решаете, когда ошибка критична, а когда — просто информация.

Небольшая иллюстрация (не про filesystem, а просто чтобы почувствовать стиль):

#include <iostream>
#include <system_error>

int main() {
    std::error_code ec;             // ec пустой (ошибки нет)
    std::cout << std::boolalpha;

    std::cout << "has error: " << static_cast<bool>(ec) << '\n'; // has error: false

    ec = std::make_error_code(std::errc::permission_denied);
    std::cout << "has error: " << static_cast<bool>(ec) << '\n'; // has error: true
    std::cout << "message: " << ec.message() << '\n';            // message: Permission denied (формулировка зависит от ОС)
}

В std::filesystem вы чаще будете получать ec от самой операции, а не создавать вручную.

Бросающие операции и try/catch

Исключения в filesystem работают логично: вызвали операцию — если не получилось, прилетело исключение, вы его поймали. Для действительно «нештатных» ситуаций это нормально. Но если вы делаете много маленьких проверок/действий, код превращается в ковёр из try/catch, а читателю хочется выйти в окно (аккуратно, через std::filesystem::path).

Типичный пример «бросающего» стиля:

#include <filesystem>
#include <iostream>

int main() {
    namespace fs = std::filesystem;

    try {
        fs::rename("missing.txt", "moved.txt");
        std::cout << "renamed\n"; // renamed
    } catch (const fs::filesystem_error& e) {
        std::cerr << "filesystem_error: " << e.what() << '\n';
    }
}

Проблема не в том, что это «плохо». Проблема в том, что для ожидаемых ошибок этот стиль слишком «громкий». Если у вас по сценарию файл может отсутствовать, вы не хотите каждый раз устраивать мини‑расследование уровня «кто украл missing.txt».

error_code‑стиль: базовый шаблон

С error_code‑перегрузками вы пишете код более «ветвящийся», но зато без исключений и с прозрачным управлением потоком. Ключевой навык — строго соблюдать порядок: сначала проверяем ec, и только потом трактуем bool/результат. Потому что некоторые функции в случае ошибки возвращают «похожее на нормальный ответ» (например, false) — и без ec вы не поймёте, это «не существует» или «не смог проверить».

Шаблон обычно такой:

#include <filesystem>
#include <iostream>
#include <system_error>

int main() {
    namespace fs = std::filesystem;

    std::error_code ec;
    fs::rename("missing.txt", "moved.txt", ec);

    if (ec) {
        std::cerr << "rename error: " << ec.message() << '\n';
        return 1;
    }

    std::cout << "renamed\n"; // renamed (если получилось)
}

Здесь важно, что rename в error_code‑стиле не бросает исключение, а просто заполняет ec.

Важная тонкость: false не всегда означает ошибку

С std::filesystem есть классический «подвох для новичка»: некоторые операции возвращают bool, где false означает «ничего не изменилось», но это может быть нормально. При этом ошибка отдельно живёт в ec. Это прямо то, из‑за чего порядок «сначала ec, потом bool» должен стать привычкой.

Показательный пример — remove. Он возвращает true, если что-то реально удалили, и false, если удалять было нечего. Это не обязано быть ошибкой.

#include <filesystem>
#include <iostream>
#include <system_error>

int main() {
    namespace fs = std::filesystem;

    std::error_code ec;
    bool removed = fs::remove("maybe_tmp.log", ec);

    if (ec) {
        std::cerr << "remove error: " << ec.message() << '\n';
        return 1;
    }

    std::cout << std::boolalpha;
    std::cout << "removed: " << removed << '\n'; // removed: false (если файла не было)
}

Смысл: removed == false может означать «файл отсутствовал, всё ок». А вот ec означает «операция не смогла корректно выполниться».

На примере exists: «не существует» vs «не смог проверить»

Функция exists кажется очень простой: есть путь или нет. Но реальность сложнее: «не смог проверить» — это третье состояние. Например, нет прав доступа к директории, где лежит файл, или путь битый с точки зрения ОС. В бросающем стиле это было бы исключение. В error_code‑стиле это отражается как ec != 0.

И вот тут появляется ловушка: exists(p, ec) в случае ошибки может вернуть false. То есть false означает и «не существует», и «не удалось проверить». Поэтому вы обязаны проверять ec.

#include <filesystem>
#include <iostream>
#include <system_error>

int main() {
    namespace fs = std::filesystem;

    std::error_code ec;
    bool ok = fs::exists("some/path", ec);

    if (ec) {
        std::cerr << "exists error: " << ec.message() << '\n';
        return 1;
    }

    std::cout << std::boolalpha << "exists: " << ok << '\n';
}

Если вы забудете if (ec), вы получите программу, которая на проблемах доступа будет говорить «ну, наверное, не существует» — и вы будете чинить не то место.

Дисциплина работы с ec: не тащить «старую ошибку» дальше

std::error_code — обычная переменная. А значит, она умеет хранить старое значение. Это звучит очевидно, но это одна из самых частых причин «странных» багов: вы сделали одну операцию, она записала ошибку, потом сделали вторую операцию (которая могла вообще не трогать ec в том месте, где вы ожидали), а вы проверили ec и думаете, что ошибка от второй операции.

Есть два хороших стиля.

Первый стиль — создавать ec на каждую операцию отдельно, прямо рядом:

std::error_code ec;
bool ok = fs::create_directories(dir, ec);
if (ec) { /* обработать */ }

Второй стиль — использовать один ec, но перед каждым вызовом делать ec.clear():

ec.clear();
fs::rename(from, to, ec);
if (ec) { /* обработать */ }

Для новичка первый стиль обычно проще: меньше шансов перепутать контекст.

3. Пример: мини‑утилита NoteVault

Чтобы не осталось ощущения «это всё теория ради теории», давайте встроим это в простое консольное приложение. Пусть это будет мини‑утилита NoteVault: заметки хранятся в директории notes/, каждая заметка — обычный текстовый файл. В прошлые дни мы бы читали/писали файл через ifstream/ofstream, а сегодня наша задача — безопасно управлять путями и файловыми операциями: создать директорию, проверить существование, переименовать, удалить — и не превращать отсутствие файла в трагедию.

Сделаем три маленьких функции: получить путь к директории заметок, гарантировать, что директория существует, и удалить заметку «мягко» (если её нет — это не ошибка).

База: create_directories без исключений

Начнём с функции EnsureNotesDir, которая создаёт notes/ при необходимости. Ошибка тут уже может быть важной: если вы вообще не можете создать рабочую директорию, приложение толком не сможет работать.

#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

fs::path NotesDir() {
    return fs::path{"notes"};
}

bool EnsureNotesDir(std::error_code& ec) {
    ec.clear();
    fs::create_directories(NotesDir(), ec);
    return !ec;
}

Обратите внимание: create_directories возвращает bool (создали или уже было), но в нашей логике важнее другое: удалось ли это сделать вообще. Поэтому функция возвращает !ec.

«Мягкое удаление»: отсутствие файла — нормальный исход

Теперь функция удаления. Идея такая: пользователь мог попросить удалить заметку, но файла может уже не быть. Это не повод падать, это повод сказать «удалять нечего».

#include <filesystem>
#include <iostream>
#include <system_error>

namespace fs = std::filesystem;

bool RemoveNote(std::string_view name, std::error_code& ec) {
    ec.clear();

    fs::path p = NotesDir() / fs::path{name};
    p.replace_extension(".txt");

    bool removed = fs::remove(p, ec);
    if (ec) return false;

    return removed; // true = удалили, false = не было
}

Заметьте приятный бонус fs::path: мы заменяем расширение через replace_extension, а не через ручной поиск точки в строке. Это как минимум экономит вам один вечер жизни.

Переименование заметки в error_code‑стиле

Переименование — операция, где ошибки могут быть ожидаемыми: исходного файла может не быть; целевой файл может уже существовать; могут быть права доступа. Мы хотим корректно показать сообщение, а не ловить исключения.

#include <filesystem>
#include <system_error>
#include <string_view>

namespace fs = std::filesystem;

bool RenameNote(std::string_view from, std::string_view to, std::error_code& ec) {
    ec.clear();

    fs::path pFrom = NotesDir() / fs::path{from};
    pFrom.replace_extension(".txt");

    fs::path pTo = NotesDir() / fs::path{to};
    pTo.replace_extension(".txt");

    fs::rename(pFrom, pTo, ec);
    return !ec;
}

Весь контракт в одном месте: если ec пустой — переименовали.

Скелет main: действия и понятные сообщения

Теперь сделаем очень простой main, который не является полноценным CLI‑парсером (это отдельная большая тема), но уже демонстрирует нормальный стиль: действие → проверка ec → сообщение человеку.

#include <filesystem>
#include <iostream>
#include <string>
#include <system_error>

namespace fs = std::filesystem;

fs::path NotesDir();
bool EnsureNotesDir(std::error_code& ec);
bool RemoveNote(std::string_view name, std::error_code& ec);
bool RenameNote(std::string_view from, std::string_view to, std::error_code& ec);

int main() {
    std::error_code ec;

    if (!EnsureNotesDir(ec)) {
        std::cerr << "Cannot create notes dir: " << ec.message() << '\n';
        return 1;
    }

    bool removed = RemoveNote("temp", ec);
    if (ec) {
        std::cerr << "Remove failed: " << ec.message() << '\n';
        return 1;
    }
    std::cout << std::boolalpha << "Removed temp: " << removed << '\n'; // Removed temp: false

    bool renamed = RenameNote("draft", "done", ec);
    if (!renamed) {
        std::cerr << "Rename failed: " << ec.message() << '\n';
        return 1;
    }
    std::cout << "Renamed draft -> done\n"; // Renamed draft -> done

    return 0;
}

Да, это пока «в лоб». Но по нему уже видно главное: мы не ловим исключения для ожидаемых ситуаций, и при этом не теряем диагностические сообщения.

4. Как выбрать: исключения или error_code

Здесь нет универсального «всегда делай так». Но есть очень практичное правило, которое хорошо работает в учебных и реальных проектах.

Если ошибка означает «мы не можем продолжать сценарий нормально» и это действительно нештатная ситуация, исключения могут быть удобны: код остаётся линейным, и вы ловите проблему «на верхнем уровне». Если же ошибка является частью обычной логики (файла может не быть, директория может быть недоступна, пользователь мог ошибиться в имени), то error_code‑перегрузки дают вам спокойный, управляемый код без постоянных try/catch.

В рамках сегодняшней темы мы тренируемся как раз во втором подходе: сделать приложение, которое не считает внешний мир обязанным быть идеальным.

5. Типичные ошибки при работе с error_code в std::filesystem

Ошибка №1: проверять только bool и игнорировать ec.
Так часто происходит с exists и remove: вы видите false и делаете вывод «не существует» или «не удалилось». Но это может быть не «не существует», а «не смог проверить», и не «не удалилось», а «ошибка доступа». Если вы не проверили ec, вы фактически отключили себе диагностику и оставили в коде загадку.

Ошибка №2: повторно использовать один и тот же std::error_code и забыть очистить.
ec не очищается сам по себе «потому что вы так задумали». Если в нём осталась старая ошибка, следующая проверка if (ec) может сработать на ровном месте, и вы будете обвинять в этом невиновный rename, хотя виноват предыдущий remove.

Ошибка №3: смешивать в одном сценарии исключения и error_code.
Технически так можно, но читать и сопровождать такое тяжело. В одном месте код «падает» исключением, в другом — тихо пишет в ec. Новичок в такой код заходит бодрым, а выходит слегка поседевшим. Обычно лучше выбрать один стиль на один сценарий.

Ошибка №4: печатать только ec.message() без контекста.
Сообщение ошибки полезно, но без пути и описания операции оно иногда малоинформативно. Хороший минимум: печатать «что делали» и «с каким путём». Иначе лог превращается в набор загадочных фраз вроде «Permission denied», как будто это не программа, а охранник в ночном клубе.

Ошибка №5: пытаться через error_code «замести под ковёр» критические ошибки.
error_code — не способ сделать вид, что проблем нет. Это способ аккуратно обработать ожидаемую проблему. Если приложение не может создать рабочую директорию или записать важный файл, это обычно надо явно сообщить и завершиться с ошибкой, а не продолжать в поломанном состоянии «ну ладно, как‑нибудь без данных».

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