Skip to Content
Course content

250: std::filesystem Path Manipulation In Depth

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was digging through some legacy build scripts the other day and found a absolute nightmare of string concatenations used to manage file paths. You know the type: if (path.back() != '/') path += '/'; scattered everywhere. It's fragile, it's ugly, and it's exactly why std::filesystem::path exists. But if you just treat it like a string, you're missing the actual power of the library.

The slash struggle

Let's say I'm building a system to organize game assets. I have a root directory, and I need to put a texture into a specific subfolder. My first instinct—the "old" way—is to just glue strings together.

std::string root = "assets";
std::string folder = "textures";
std::string file = "hero_diffuse.png";
std::string full_path = root + "/" + folder + "/" + file; 
// "assets/textures/hero_diffuse.png"

This works on Linux. But what happens if root already ends with a slash? I get assets//textures/.... It's a mess. Let's see how std::filesystem::path handles this. I'll try using the / operator, which is specifically overloaded for path joining.

namespace fs = std::filesystem;
fs::path root = "assets";
fs::path folder = "textures";
fs::path file = "hero_diffuse.png";

fs::path full_path = root / folder / file;
// Result: "assets/textures/hero_diffuse.png" (on Unix)
// Result: "assets\\textures\\hero_diffuse.png" (on Windows)

The magic here is that the / operator doesn't just append a character; it understands the concept of a directory separator. If I change root to "assets/" (with a trailing slash), the operator is smart enough not to add a second one. It just works. It handles the platform-specific separators for me, which is a huge relief when you aren't wanting to write #ifdef _WIN32 every five lines.

Dissecting the filename

Now, let's get into the weeds. Suppose my asset pipeline generates temporary files with suffixes, like hero_diffuse.png.tmp. I need to strip that .tmp, keep the .png, and maybe change the folder.

I'll start by trying to grab the extension. I'm expecting .png, but let's see what actually happens:

fs::path p = "assets/textures/hero_diffuse.png.tmp";
std::cout << p.extension(); // Output: ".tmp"

Ah, right. The extension is defined as the part starting from the last dot. That's not what I wanted. I want the "stem"—the part before the extension. Let's try p.stem().

std::cout << p.stem(); // Output: "hero_diffuse.png"

Okay, so the stem of hero_diffuse.png.tmp is hero_diffuse.png. If I want to get rid of that .tmp and actually "promote" the file to its final version, I can use replace_extension(). But wait, if I call p.replace_extension(".png") on hero_diffuse.png.tmp, I'm just swapping .tmp for .png, leaving me with hero_diffuse.png.png. That's not right.

The trick here is that I need to treat the stem as a path itself if I have double extensions. Or, more simply, I can just pop the extension off entirely:

fs::path p = "assets/textures/hero_diffuse.png.tmp";
p.replace_extension(""); // Now it's "assets/textures/hero_diffuse.png"

Now the file is clean. If I wanted to move this to a "processed" folder, I can use parent_path() to isolate the directory and then build a new path.

Cleaning up the "dot-dot" mess

One thing that always trips people up is when paths get "dirty" with .. (parent directory) or . (current directory) references. This happens a lot when you're resolving relative paths from a config file.

I'll try to create a path that's a bit of a disaster:

fs::path messy = "assets/textures/../sounds/./ambient/wind.wav";
std::cout << messy; // Output: "assets/textures/../sounds/./ambient/wind.wav"

The path object just stores the string as-is. It doesn't automatically resolve the .. because doing so would require checking the actual disk (since a directory could be a symbolic link, and .. from a symlink might lead somewhere unexpected).

But what if I just want the logical simplification? I don't want to touch the disk; I just want the path to look sane. That's where lexically_normal() comes in. Let's try it:

fs::path clean = messy.lexically_normal();
std::cout << clean; // Output: "assets/sounds/ambient/wind.wav"

That's much better. Now, if I actually needed to know where this file lives on the physical drive—resolving all symlinks and absolute paths—I'd use std::filesystem::canonical(). But be careful: canonical() throws an exception if the file doesn't actually exist. lexically_normal() is just string manipulation on steroids; it doesn't care if the folder exists or not.




📋 Practical Task

The Asset Pipeline Path Sanitizer

You are writing a utility for a game engine that cleans up a list of "dirty" asset paths. You are given a std::vector<std::string> of paths that may contain ., .., and temporary extensions (.tmp).

Write a function std::vector<std::string> sanitizeAssets(const std::vector<std::string>& rawPaths) that performs the following transformations on each path:

  • Convert the string to a std::filesystem::path.
  • If the path ends in .tmp, remove that extension.
  • Normalize the path (resolve . and .. logically).
  • Ensure the path is relative to a folder named "final_build" (meaning you should prepend "final_build/" to the normalized path).
  • Return the result as a vector of strings.

Example Input: "assets/textures/../models/player.obj.tmp"
Expected Output: "final_build/assets/models/player.obj"

Rating
0 0

There are no comments for now.

to be the first to leave a comment.