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
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Posted

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 from 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 are available that return true or false based on the existence of any of these path parts.

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 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 seems like the provided string has a file name.

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.

But if we wanted to access files in a location relative to where our program is installed, for example, we don’t necessarily know where that is in advance. For this, we have relative paths.

Relative paths are based on some other directory, sometimes called 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. This sets a new current path, that our relative paths will be based upon:

#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 something else
  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?

Edit History

  • First Published

Ryan McCombe
Ryan McCombe
Posted
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.

7a.jpg
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
7a.jpg
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:

  • 114 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
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved