1. Код с последствиями
Когда мы только учились выводить строки через std::cout, максимум, что могло пострадать — наше самолюбие и консоль. С файловой системой всё интереснее: вы трогаете реальные директории и файлы, которые существуют вне программы. Это значит, что каждая операция — маленький договор с реальностью, а реальность любит внезапно менять условия: нет прав, файл занят, папка уже есть, папки нет, путь не туда, а пользователь вообще запускал программу из неожиданного каталога.
Полезно держать в голове простую ментальную модель: мы делаем операции двух типов. Одни создают/перемещают объекты (create_directories, copy, rename), другие удаляют (remove). И у каждой операции есть два слоя результата: «что получилось логически» и «не случилась ли ошибка». Сегодня научимся пользоваться самими «движениями руками» по файловой системе — аккуратно и осмысленно.
Для ориентира — мини-таблица, что мы сегодня трогаем:
| Операция | Что делает | Типичный “живой” сценарий |
|---|---|---|
|
Создаёт цепочку папок | Подготовить out/logs/ перед запуском |
|
Делает копию файла (или набора объектов) | Бэкап config.json → config.bak |
|
Переименовать/переместить | Ротация логов: app.log → app.log.1 |
|
Удалить файл или пустую папку | Очистить временный файл |
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 — более общий механизм, который может работать не только с одиночными файлами (например, копировать директории по выбранным правилам). Но именно из-за универсальности он требует ещё более ясного выбора опций, иначе вы получаете непредсказуемый сценарий: что делать с существующими файлами, копировать ли рекурсивно и так далее.
Для читабельности полезно держать такую «памятку опций» (ровно на уровне идеи):
| Опция | Интуитивный смысл |
|---|---|
|
«если уже есть — не трогай» |
|
«если уже есть — замени» |
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. Поэтому в прикладном коде полезно сначала понимать контракт функции: что означает её возвращаемое значение, а что относится к ошибкам выполнения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ