JavaRush /Курсы /C++ SELF /Операции std::filesystem

Операции std::filesystem

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

1. Код с последствиями

Когда мы только учились выводить строки через std::cout, максимум, что могло пострадать — наше самолюбие и консоль. С файловой системой всё интереснее: вы трогаете реальные директории и файлы, которые существуют вне программы. Это значит, что каждая операция — маленький договор с реальностью, а реальность любит внезапно менять условия: нет прав, файл занят, папка уже есть, папки нет, путь не туда, а пользователь вообще запускал программу из неожиданного каталога.

Полезно держать в голове простую ментальную модель: мы делаем операции двух типов. Одни создают/перемещают объекты (create_directories, copy, rename), другие удаляют (remove). И у каждой операции есть два слоя результата: «что получилось логически» и «не случилась ли ошибка». Сегодня научимся пользоваться самими «движениями руками» по файловой системе — аккуратно и осмысленно.

Для ориентира — мини-таблица, что мы сегодня трогаем:

Операция Что делает Типичный “живой” сценарий
create_directories(dir)
Создаёт цепочку папок Подготовить out/logs/ перед запуском
copy(...) / copy_file(...)
Делает копию файла (или набора объектов) Бэкап config.jsonconfig.bak
rename(from, to)
Переименовать/переместить Ротация логов: app.logapp.log.1
remove(path)
Удалить файл или пустую папку Очистить временный файл

2. Основные операции std::filesystem

База для всех примеров: подключение <filesystem> и псевдоним fs

Перед тем как «шевелить» файловую систему, стоит договориться о стиле кода, иначе вы утонете в длинных именах. Типичный учебный компромисс: писать namespace fs = std::filesystem;, чтобы и коротко, и понятно.

Ещё одна привычка: выводить пути через .string(), чтобы не спорить с форматированием и не удивляться странным кавычкам/экранированию в разных средах.

Скелет программы, который будет общим для примеров в этой лекции, выглядит так:

#include <filesystem>
#include <iostream>

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

    fs::path base{"demo_workspace"};
    std::cout << base.string() << '\n'; // demo_workspace
}

Пока это выглядит скучно, но именно с такого «чистого стола» удобно начинать любые операции: у вас есть fs::path, а дальше вы решаете, что сделать с файловой системой вокруг него.

create_directories: создаём папки сразу цепочкой

В реальных программах папки почти никогда не бывают одиночными. Обычно вы хотите что-то вроде out/logs/ или storage/users/. И вот тут новичок часто начинает городить велосипеды: «если нет out, создам; потом если нет logs, создам…».

std::filesystem как раз избавляет от этого: create_directories создаёт всю цепочку сразу и возвращает bool, который сообщает, было ли реально создано что-то новое.

Простейший пример: подготовим папку для логов.

#include <filesystem>
#include <iostream>

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

    bool created = fs::create_directories("demo_workspace/out/logs");
    std::cout << std::boolalpha << created << '\n'; // true (или false, если уже было)
}

Здесь важна тонкость: false — это не «провал». Чаще всего false означает «папки уже существовали, мне нечего создавать». Ошибка — это отдельная история: например, нет прав или путь «ломаный». Операции над ФС могут не выполниться по причинам, не зависящим от вашей логики.

Чтобы наш код был чуть менее «одноразовым», заведём маленькую функцию-помощник для будущего мини-приложения:

#include <filesystem>

namespace fs = std::filesystem;

bool ensure_dir(const fs::path& dir) {
    return fs::create_directories(dir);
}

Да, функция короткая. И да, она кажется «лишней». Но такие точки помогают централизовать поведение (например, как логировать ошибки), не размазывая одно и то же по всему проекту.

copy и copy_file: копия — это копия

Копирование файлов — вечная классика. Самый частый сценарий в учебных проектах: сделать резервную копию конфигурации, отчёта или базы данных перед тем, как что-то менять.

В std::filesystem для этого есть как минимум два пути: более общий copy и более «прямой» copy_file. В этой лекции нас интересует практичный минимум: копируем файл в файл и явно проговариваем политику, что делать при конфликте имён.

Сценарий: у нас есть config.txt, и мы хотим рядом положить config.bak. В прошлой лекции про path мы учились менять расширение через replace_extension(), и это отличный повод применить навык, не прибегая к строковым трюкам.

#include <filesystem>
#include <iostream>

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

    fs::path src{"demo_workspace/config.txt"};
    fs::path dst = src;
    dst.replace_extension(".bak");

    fs::copy_file(src, dst, fs::copy_options::overwrite_existing);
    std::cout << dst.string() << '\n'; // demo_workspace/config.bak
}

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

Чуть шире: fs::copy — более общий механизм, который может работать не только с одиночными файлами (например, копировать директории по выбранным правилам). Но именно из-за универсальности он требует ещё более ясного выбора опций, иначе вы получаете непредсказуемый сценарий: что делать с существующими файлами, копировать ли рекурсивно и так далее.

Для читабельности полезно держать такую «памятку опций» (ровно на уровне идеи):

Опция Интуитивный смысл
skip_existing
«если уже есть — не трогай»
overwrite_existing
«если уже есть — замени»

rename: переименовать и/или переместить

Переименование — это операция, которую люди часто недооценивают, потому что слово rename звучит как «поменять пару букв». На практике fs::rename(from, to) делает более сильную вещь: это переименование пути, которое часто является также перемещением (например, из temp/ в out/).

В идеальном мире это быстрый «переезд таблички», а не копирование содержимого. В реальном мире иногда всплывают ограничения (например, разные файловые системы), но сегодня нам важна базовая семантика: rename — это не «сделай копию», это «переставь объект в другое место».

Самый понятный сценарий — ротация логов. Представим, что у нас есть app.log, и перед новым запуском мы хотим превратить его в app.log.1.

#include <filesystem>
#include <iostream>

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

    fs::path log{"demo_workspace/out/logs/app.log"};
    fs::path old{"demo_workspace/out/logs/app.log.1"};

    fs::rename(log, old);
    std::cout << "rotated\n"; // rotated
}

Здесь есть типичный практический вопрос: «А если app.log.1 уже существует?» Тогда rename может не дать вам перезаписать цель, и это нормально: библиотека не хочет молча уничтожать ваши данные. Поэтому, если вы проектируете «ротацию», вы обязаны продумать политику: удалять старый .1, хранить .2, или вообще добавлять дату.

remove: удаление — это не всегда “успех/провал”

Удаление кажется простым: «удали файл». Но у fs::remove(path) есть важная образовательная особенность: она возвращает bool. И этот bool означает не «ошибки не было», а «удалилось ли что-то по факту». Например, если файла уже нет, remove обычно возвращает false. Это не ошибка, это ситуация «удалять нечего». Ошибка — отдельная история (например, нет прав, файл занят, директория не пуста), и она обрабатывается отдельно.

Мини-пример: пытаемся удалить временный файл.

#include <filesystem>
#include <iostream>

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

    bool removed = fs::remove("demo_workspace/tmp.txt");
    std::cout << std::boolalpha << removed << '\n'; // true или false
}

Смысловой вывод: false — не повод паниковать. Иногда это даже желаемый результат: «убедиться, что файла нет». В таких задачах remove работает как «прибраться», а не как «победить дракона».

На будущее держите в голове, что существует и «тяжёлая артиллерия» для удаления деревьев директорий, но её мы сегодня сознательно не разворачиваем: удалять всё рекурсивно — это то место, где одна ошибка может превратиться в легенду, которую будут пересказывать в чате команды («кто удалил прод?»).

3. Мини-приложение: ProjectVault

Чтобы знания не остались «на уровне отдельных заклинаний», соберём учебный сюжет: утилита ProjectVault, которая обслуживает рабочую папку проекта. Она не полноценный файловый менеджер, а маленький набор бытовых действий: создать структуру директорий, сделать резервную копию конфигурации, повернуть лог, удалить временный файл.

Сначала определим «географию» проекта: базовую папку и несколько путей внутри неё. Тут очень помогает fs::path и склейка через operator/.

#include <filesystem>

namespace fs = std::filesystem;

struct AppPaths {
    fs::path base{"demo_workspace"};
    fs::path out = base / "out";
    fs::path logs = out / "logs";
    fs::path config = base / "config.txt";
};

Теперь напишем шаг «инициализация»: создадим папки out/ и out/logs/. Мы используем create_directories, потому что хотим, чтобы функция была идемпотентной: запускать её можно много раз.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void init_dirs(const fs::path& logs_dir) {
    bool created = fs::create_directories(logs_dir);
    std::cout << std::boolalpha << "created: " << created << '\n';
    // created: true (или false)
}

Дальше — бэкап конфига. Тут применим replace_extension и copy_file с явной политикой перезаписи (в учебном сценарии бэкап можно обновлять).

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void backup_config(fs::path config) {
    fs::path bak = config;
    bak.replace_extension(".bak");

    fs::copy_file(config, bak, fs::copy_options::overwrite_existing);
    std::cout << "backup: " << bak.string() << '\n';
    // backup: demo_workspace/config.bak
}

Теперь — ротация лога. Это место специально «не идеальное»: мы не делаем несколько поколений, не добавляем дату, не решаем конфликт .1. Наша задача — увидеть смысл rename как перемещения имени.

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void rotate_log(const fs::path& logs_dir) {
    fs::path cur = logs_dir / "app.log";
    fs::path old = logs_dir / "app.log.1";

    fs::rename(cur, old);
    std::cout << "log rotated\n"; // log rotated
}

И наконец — уборка временного файла. Мы печатаем результат bool, чтобы не путать «не было файла» с «не удалось удалить».

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void clean_tmp(const fs::path& base) {
    bool removed = fs::remove(base / "tmp.txt");
    std::cout << std::boolalpha << "tmp removed: " << removed << '\n';
    // tmp removed: true (или false)
}

Осталось связать это в main. Мы сделаем простой выбор действия через ввод строки. Это не «настоящие CLI-аргументы», а просто дружелюбное меню.

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

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

    fs::path base{"demo_workspace"};
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "init") fs::create_directories(base / "out/logs");
    if (cmd == "clean") fs::remove(base / "tmp.txt");
}

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

4. Типичные ошибки

Ошибка №1: склеивать пути строками, а потом удивляться, почему не работает на другой ОС.
Очень часто код выглядит как base + "/out/" + name, и пока вы запускаете его в одной среде, всё кажется нормальным. Потом выясняется, что где-то лишний слэш, где-то забыли слэш, где-то двойной слэш, а где-то вообще другая семантика разделителей. Лекарство простое: представлять путь типом fs::path и склеивать компоненты через operator/, а строковое представление доставать только на вывод.

Ошибка №2: ждать, что create_directories “создаст папку” и обязательно вернёт true.
Новичок видит bool и автоматически думает «успех/провал». На самом деле false часто означает «всё уже создано до нас». В таких операциях false — не повод ставить красный флажок «ошибка», это часто нормальный режим работы: программа повторно запускается в уже подготовленной структуре.

Ошибка №3: считать, что copy/copy_file сами угадают вашу политику конфликта имён.
Если вы не формулируете, что делать при существующем файле назначения, вы оставляете решение библиотеке и условиям вызова, а потом получаете поведение, которое тяжело объяснить пользователю. Именно поэтому copy_options — это не «занудство», а способ сделать намерение явным.

Ошибка №4: путать rename и копирование.
Иногда пишут «сделаю копию через rename», а потом удивляются, что исходный файл исчез. rename — это «переставить объект», а не «сделать второй». Если вам нужна копия — используйте copy_file или copy. Если вам нужно переименовать/переместить — rename.

Ошибка №5: интерпретировать remove() == false как “ошибка удаления”.
Для remove значение false часто означает «по этому пути ничего не было». Это полезно, когда вы хотите просто привести систему в состояние «файла нет». Ошибка — это другое событие, и оно не кодируется одним лишь bool. Поэтому в прикладном коде полезно сначала понимать контракт функции: что означает её возвращаемое значение, а что относится к ошибкам выполнения.

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