File System Paths

A guide to effectively working with file system paths, using the std::filesystem::path type.

Ryan McCombe
Updated

In the previous lessons, we represented our file paths as simple strings, but the standard library provides a dedicated class for this: std::filesystem::path.

This type provides additional utility specific to working with the file system. For brevity, we'll alias std::filesystem to fs in the code examples in this lesson:

namespace fs = std::filesystem;

Creating fs::path Objects

We can create fs::path objects using simple strings:

#include <filesystem>
namespace fs = std::filesystem;

int main() {
  fs::path Location{R"(c:\test)"};
}

We can get the string representation of a path using the string() method, which is useful when we want to display it:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path Location{R"(c:\test)"};

  std::cout << Location.string();
}
c:\test

Using fs::path Objects with Directory Entries

The fs::directory_entry constructor we've been using in the previous lesson accepts an fs::path argument:

#include <filesystem>
namespace fs = std::filesystem;

int main() {
  fs::path Location{R"(c:\test)"};
  fs::directory_entry File{Location};
}

Since fs::path can be created from a string, our fs::path objects were being created implicitly:

#include <filesystem>
namespace fs = std::filesystem;

int main() {
  // Implicitly converting raw string to fs::path
  fs::directory_entry File{R"(c:\test)"};
}

We can get the fs::path associated with a fs::directory_entry using the path() method:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::directory_entry Entry{R"(c:\test)"};

  std::cout << Entry.path().string();
}
c:\test

Accessing fs::path Components

A variety of methods give us access to specific parts of the path. These also return fs::path objects, so in the following example we use the string() method to display them:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path Location{R"(c:\test\hello.txt)"};

  std::cout << "File Name: "
            << Location.filename().string();

  std::cout << "\nFile Stem: "
            << Location.stem().string();

  std::cout << "\nFile Extension: "
            << Location.extension().string();

  std::cout << "\nParent Path: "
            << Location.parent_path().string();

  std::cout << "\nRoot Path: "
            << Location.root_name().string();
}
File Name: hello.txt
File Stem: hello
File Extension: .txt
Parent Path: c:\test
Root Path: c:

Equivalent boolean methods return true or false based on the existence of any of these path components.

We can access these by prepending has_ to the method names. For example, has_filename() and has_extension()

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path Directory{R"(c:\test\)"};
  if (!Directory.has_filename()) {
    std::cout << Directory.string()
              << " has no file name\n";
  }

  fs::path File{R"(c:\hi.txt)"};
  if (File.has_extension()) {
    std::cout << File.string()
              << " has a file extension";
  }
}
c:\test\ has no file name
c:\hi.txt has a file extension

Note that the result of these functions is based only on the format of the provided string. For example, the has_filename() method returns true if it appears that the provided string has a filename.

To access the file system and check whether there really is a file at that path, we need to create a fs::directory_entry, not just a fs::path. We can then call a method like is_regular_file(), as we covered in the previous lesson.

Working with fs::path File Names

When working with paths, a common requirement is to manipulate the file name. We have some methods to help us there:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path File{R"(c:\test\hello.txt)"};
  std::cout << File.string() << '\n';

  File.replace_filename("world.txt");
  std::cout << File.string() << '\n';

  File.replace_extension("doc");
  std::cout << File.string() << '\n';

  File.remove_filename();
  std::cout << File.string();
}
c:\test\hello.txt
c:\test\world.txt
c:\test\world.doc
c:\test\

Use cases for these methods typically come up when we're creating reusable functions.

For example, the following function creates a file, but the location of the file it creates is derived from an argument. In this case, it will create the file c:\test\hello.backup:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

void CreateBackup(const fs::path& Path) {
  fs::path Backup{Path};
  Backup.replace_extension("backup");
  fs::copy_file(Path, Backup);
}

int main() {
  CreateBackup(R"(c:\test\hello.txt)");
}

Appending to an fs::path

The fs::path type also overrides the /= operator, which allows us to create paths to subdirectories or files. This is done by automatically appending separators that are appropriate to the underlying operating system:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path File{R"(c:\)"};
  std::cout << File.string() << '\n';

  File /= "test";
  std::cout << File.string() << '\n';

  File /= "hello.txt";
  std::cout << File.string() << '\n';
}
c:\
c:\test
c:\test\hello.txt

This operator, and most of the fs::path methods, returns a reference to the original object. This allows them to be chained:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path File{R"(c:\test\hello.txt)"};
  std::cout << File.string() << '\n';

  File.remove_filename() /= "subdirectory";  
  std::cout << File.string() << '\n';

  (File /= "nested") /= "directory";
  std::cout << File.string();
}
c:\test\hello.txt
c:\test\subdirectory
c:\test\subdirectory\nested\directory

Relative Paths

All the paths we've shown so far have been absolute paths. If we wanted to access files in an exact location, we should use absolute paths.

However, if we want to access files in a location relative to where our program is installed, we don't necessarily know the exact location in advance. For this, we use relative paths.

Relative paths are based on another directory, often referred to as the current path or current working directory.

We can check if a path is relative or absolute using the is_relative() and is_absolute() methods:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::path A{R"(c:\test\hello.txt)"};
  if (A.is_absolute()) {
    std::cout << "A is Absolute";
  }

  fs::path B{R"(hello.txt)"};
  if (B.is_relative()) {
    std::cout << "B is Relative";
  }
}
A is Absolute
B is Relative

We can retrieve the current path that relative paths are based on using the current_path() method. Its default value depends on our settings, but we can pass a new path to that function to set a new current path for our relative paths:

#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

int main() {
  fs::directory_entry Entry{R"(hello.txt)"};

  std::cout
      << "The default working directory is:\n"
      << fs::current_path().string();

  if (!Entry.exists()) {
    std::cout << "\nThe file was not found\n\n";
  }

  // Setting the current path to a new lcoation
  fs::current_path(R"(c:\test)");

  std::cout << "The current working directory "
               "was changed to:\n"
            << fs::current_path().string();

  if (Entry.exists()) {
    std::cout << "\nThe file was found!";
  }
}
The working directory is:
C:\Users\ryan\repos\cpp
The file was not found

The working directory was changed to:
c:\test
The file was found!

Summary

In this lesson, we explored the versatile capabilities of std::filesystem::path, demonstrating how to create, manipulate, and use file paths effectively. We covered various operations from basic path creation to advanced manipulations.

Key Takeaways

  • Learned to create and use fs::path objects for representing and manipulating file paths.
  • Explored methods to access and modify different components of a file path, like filename, extension, and parent path.
  • Discovered how to append subdirectories or files to a path using the /= operator, with examples demonstrating path chaining.
  • Understood the distinction between absolute and relative paths and how to determine the type of a given path.
  • Gained insights into setting and retrieving the current working directory using fs::current_path().
Next Lesson
Lesson 123 of 128

Directory Iterators

An introduction to iterating through the file system, using directory_iterator and recursive_directory_iterator.

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Handling File Paths with Spaces
How do I handle file paths with spaces using std::filesystem::path?
Using Network Paths and URLs
Can std::filesystem::path work with network paths or URLs?
Wide String Conversion
How do I convert std::filesystem::path to a wide string for use with Windows APIs?
Handling Invalid Paths
What happens if I pass an invalid path to std::filesystem::path?
Using Symbolic Links
Can I use std::filesystem::path to work with symbolic links?
Relative Paths Between Absolute Paths
How do I find the relative path between two absolute paths?
Storing Path Objects in Containers
Can I store std::filesystem::path objects in a container like std::vector?
Renaming Files and Directories
How do I rename a file or directory using std::filesystem::path?
Dealing with Case Sensitivity
How do I deal with case sensitivity in file paths?
Using Environment Variables
How can I combine std::filesystem::path with environment variables to form paths?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant