File System Paths

A guide to effectively working with file system paths, using the std::filesystem::path type.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
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. We’ll alias it to fs::path in this lesson.

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().

Was this lesson useful?

Next Lesson

Directory Iterators

An introduction to iterating through the file system, using directory iterators and recursive directory iterators
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

File System Paths

A guide to effectively working with file system paths, using the path type within the standard library's filesystem module.

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Files and Serialization
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 125 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Directory Iterators

An introduction to iterating through the file system, using directory iterators and recursive directory iterators
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved