CodeGym /Courses /C++ SELF /std::filesystem operations

std::filesystem operations

C++ SELF
Level 64, Lesson 2
Available

1. Code with consequences

When we first learned to print strings via std::cout, the worst that could suffer was our pride and the console. With the file system it's more interesting: you touch real directories and files that exist outside the program. That means every operation is a small contract with reality, and reality likes to change conditions unexpectedly: no permissions, file in use, folder already exists, folder missing, path pointing elsewhere, or the user ran the program from an unexpected directory.

It's useful to keep a simple mental model in mind: we perform two kinds of operations. Some create/move objects (create_directories, copy, rename), others remove (remove). And every operation has two layers of result: “what logically happened” and “did an error occur”. Today we'll learn how to perform filesystem “hand movements” carefully and meaningfully.

For orientation — a mini-table of what we touch today:

Operation What it does Typical “live” scenario
create_directories(dir)
Creates a chain of folders Prepare out/logs/ before startup
copy(...) / copy_file(...)
Makes a copy of a file (or a set of objects) Backup config.json → config.bak
rename(from, to)
Rename/move Log rotation: app.log → app.log.1
remove(path)
Remove a file or an empty folder Clean a temporary file

2. Core operations of std::filesystem

Base for all examples: include <filesystem> and alias fs

Before you “poke” the filesystem, agree on a coding style, otherwise you'll drown in long names. A common educational compromise is to write namespace fs = std::filesystem;, so it's both short and clear.

Another habit: print paths via .string() to avoid fighting formatting and surprising quotes/escaping in different environments.

The program skeleton common to examples in this lecture looks like this:

#include <filesystem>
#include <iostream>

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

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

It looks boring so far, but starting from a “clean desk” is convenient for any operations: you have an fs::path, and then decide what to do with the filesystem around it.

create_directories: create folders as a chain

In real programs folders are almost never single. Usually you want something like out/logs/ or storage/users/. Newcomers often reinvent the wheel: “if out doesn't exist, create it; then if logs doesn't exist, create it…”.

std::filesystem relieves that: create_directories creates the whole chain at once and returns a bool that tells whether something new was actually created.

Simple example: prepare a folder for logs.

#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 (or false if already existed)
}

Here is an important nuance: false is not a “failure”. Most often false means “the directories already existed, there was nothing to create”. An error is a different story: for example, lack of permissions or a malformed path. Filesystem operations can fail for reasons outside your logic.

To make our code a bit less “one-off”, let's create a small helper function for the future mini-application:

#include <filesystem>

namespace fs = std::filesystem;

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

Yes, the function is short. And yes, it may seem “redundant”. But such points help centralize behavior (for example, how to log errors) instead of scattering the same logic across the project.

copy and copy_file: a copy is a copy

File copying is a classic. The most common scenario in learning projects: make a backup of a configuration, report, or database before changing something.

In std::filesystem there are at least two ways: the more general copy and the more “direct” copy_file. In this lecture we focus on the practical minimum: copy a file to a file and explicitly state the policy for name conflicts.

Scenario: we have config.txt, and we want to place next to it config.bak. In the previous lecture about path we learned to change an extension via replace_extension(), and it's a good opportunity to apply that skill without string tricks.

#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
}

Why explicitly specify copy_options::overwrite_existing? Because “overwrite or not” is a business decision. If you don't formulate it, the program will behave “however it ends up”, and the user may later say: “it overwrote my important file”. And they will be right, formally.

Broader view: fs::copy is a more general mechanism that can work not only with single files (for example, copy directories according to chosen rules). But due to this generality it requires even clearer option choices, otherwise you get unpredictable behavior: what to do with existing files, whether to copy recursively, and so on.

For readability it's useful to keep a little “options cheat-sheet” (just the idea level):

Option Intuitive meaning
skip_existing
“if it already exists — don't touch”
overwrite_existing
“if it already exists — replace”

rename: rename and/or move

Renaming is an operation people often underestimate, because the word rename sounds like “change a couple of letters”. In practice fs::rename(from, to) does a stronger thing: it's a path rename which is often also a move (for example, from temp/ to out/).

In an ideal world it's a fast “swap of a name tag”, not copying the content. In the real world there may be limitations (for example, different filesystems), but today what's important is the basic semantics: rename is not “make a copy”, it's “move the object to another place”.

The clearest scenario is log rotation. Suppose we have app.log, and before a new run we want to turn it into 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
}

There is a common practical question: “What if app.log.1 already exists?” Then rename may not overwrite the target, and that's OK: the library doesn't want to silently destroy your data. Therefore, if you design a rotation you must decide the policy: delete the old .1, keep .2, or add a date.

remove: removal is not always “success/failure”

Removal seems simple: “delete the file”. But fs::remove(path) has an important educational feature: it returns a bool. And that bool does not mean “no error occurred”, it means “did something actually get removed”. For example, if the file is already absent, remove usually returns false. That's not an error, it's “nothing to remove”. An error is a different story (for example, lack of permissions, file in use, directory not empty), and it's handled separately.

Mini-example: try to remove a temporary file.

#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 or false
}

Meaningful takeaway: false is not a reason to panic. Sometimes it's even the desired result: “ensure the file is gone”. In such tasks remove works like “clean up”, not like “slay a dragon”.

Keep in mind there is also “heavy artillery” for removing directory trees, but we intentionally don't cover it today: deleting recursively is where a single mistake can become a team legend (“who removed prod?”).

3. Mini-application: ProjectVault

So the knowledge doesn't stay “as isolated spells”, let's assemble a teaching scenario: the utility ProjectVault, which services a project's workspace. It's not a full file manager, but a small set of everyday actions: create directory structure, back up configuration, rotate a log, remove a temporary file.

First define the project's “geography”: a base folder and several paths inside it. fs::path and joining via operator/ help a lot here.

#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";
};

Now write the initialization step: create out/ and out/logs/. We use create_directories because we want the function to be idempotent: it can be run many times.

#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 (or false)
}

Next — config backup. Here we apply replace_extension and copy_file with an explicit overwrite policy (in a teaching scenario updating the backup is acceptable).

#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
}

Now — log rotation. This place is intentionally “not perfect”: we don't create multiple generations, don't add a date, and don't resolve a conflict for .1. Our goal is to see rename as moving a name.

#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
}

Finally — cleaning the temporary file. We print the bool result so we don't confuse “file wasn't there” with “failed to delete”.

#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 (or false)
}

All that's left is to wire this up in main. We'll make a simple action selection via a line of input. This isn't “real CLI args”, just a friendly menu.

#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");
}

Yes, it's still “straightforward” and without functions — it's important we feel that filesystem operations are normal function calls. In next steps we'll do this more maturely: unified error handling style and safer directory traversal patterns.

4. Typical mistakes

Mistake #1: concatenating paths as strings and then wondering why it fails on another OS.
Very often code looks like base + "/out/" + name, and while it runs in your environment everything seems fine. Later it turns out there is an extra slash somewhere, a missing slash somewhere, double slashes somewhere, or a different separator semantic. The cure is simple: represent paths with fs::path and join components with operator/, and only get a string representation for output.

Mistake #2: expecting create_directories to “create a folder” and always return true.
A newcomer sees bool and automatically thinks “success/failure”. In reality false often means “everything was already created before us”. In such operations false is not a reason to flag an error; it's often a normal mode of operation: the program is restarted in an already prepared structure.

Mistake #3: assuming copy/copy_file will guess your name-conflict policy.
If you don't state what to do when the destination file exists, you leave the decision to the library and calling conditions, and later get behavior that's hard to explain to the user. That's why copy_options is not “pedantry”, it's a way to make intent explicit.

Mistake #4: confusing rename with copying.
Sometimes people write “I'll make a copy by rename” and then wonder why the original file disappeared. rename is “move the object”, not “make a second one”. If you need a copy — use copy_file or copy. If you need to rename/move — use rename.

Mistake #5: interpreting remove() == false as “deletion error”.
For remove the value false often means “there was nothing at that path”. This is useful when you want to ensure the system state is “file absent”. An error is a different event and isn't encoded by that single bool. So in application code it's useful to first understand the function's contract: what its return value means and what belongs to runtime errors.

1
Task
C++ SELF, level 64, lesson 2
Locked
Report directories
Report directories
1
Task
C++ SELF, level 64, lesson 2
Locked
Backup Note
Backup Note
1
Task
C++ SELF, level 64, lesson 2
Locked
Log rotation
Log rotation
1
Task
C++ SELF, level 64, lesson 2
Locked
Vault commands
Vault commands
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION