LOADING

加载过慢请开启缓存 浏览器默认开启

C++ Unleashed: Filesystem

C++ Unleashed: From Zero to Hero

Previous chapter: C++20 Features

Go to Table of Contents

Filesystem

The Filesystem library, introduced in C++17 and further enhanced in C++20, provides a standardized and portable way to perform file and directory operations. It abstracts away the underlying operating system’s file handling mechanisms, allowing developers to interact with the filesystem using a consistent and intuitive API. This chapter delves into the capabilities of std::filesystem, enabling you to manage files and directories efficiently in your C++ applications.

Table of Contents

  1. Introduction to std::filesystem
  2. Getting Started with std::filesystem
  3. Path Manipulation
  4. File Operations
  5. Directory Operations
  6. Iterating Through Directories
  7. Querying File Properties
  8. Error Handling in Filesystem Operations
  9. Best Practices with std::filesystem
  10. Practical Examples
  11. Summary

Introduction to std::filesystem

What is std::filesystem?

std::filesystem is a library in the C++ Standard Library that provides facilities to perform operations on files and directories, such as creation, deletion, modification, and querying of file metadata. It offers a modern, type-safe, and cross-platform interface for interacting with the filesystem, eliminating the need to rely on platform-specific APIs or workarounds.

Why Use std::filesystem?

  • Portability: Write code that works seamlessly across different operating systems (Windows, Linux, macOS).
  • Type Safety: Utilize strong type abstractions like std::filesystem::path to manage filesystem paths.
  • Rich Functionality: Access a wide range of filesystem operations, from simple file manipulations to complex directory traversals.
  • Modern API: Leverage modern C++ features such as exceptions, smart pointers, and RAII (Resource Acquisition Is Initialization) for robust code.

Including std::filesystem

To use std::filesystem, include the <filesystem> header and use the std::filesystem namespace or create an alias for convenience.

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

Note: Ensure that your compiler supports C++17 or later standards and that you enable the appropriate language flags (e.g., -std=c++17 for GCC and Clang).


Getting Started with std::filesystem

Before diving into specific operations, it’s essential to understand the primary components of the std::filesystem library.

Key Components

  1. std::filesystem::path: Represents filesystem paths, handling differences in path syntax across operating systems.
  2. std::filesystem::directory_entry: Represents an entry within a directory, providing access to file metadata.
  3. std::filesystem::directory_iterator & std::filesystem::recursive_directory_iterator: Facilitate iteration over directory contents.

Basic Example: Creating and Removing a Directory

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path dir = "example_dir";

    // Create a directory
    try {
        if(fs::create_directory(dir)) {
            std::cout << "Directory created: " << dir << std::endl;
        } else {
            std::cout << "Directory already exists: " << dir << std::endl;
        }
    } catch(const fs::filesystem_error& e) {
        std::cerr << "Error creating directory: " << e.what() << std::endl;
    }

    // Remove the directory
    try {
        if(fs::remove(dir)) {
            std::cout << "Directory removed: " << dir << std::endl;
        } else {
            std::cout << "Directory does not exist: " << dir << std::endl;
        }
    } catch(const fs::filesystem_error& e) {
        std::cerr << "Error removing directory: " << e.what() << std::endl;
    }

    return 0;
}

Output:

Directory created: "example_dir"
Directory removed: "example_dir"

Explanation:

  • Creating a Directory: Uses fs::create_directory to create a new directory. If the directory already exists, it returns false.
  • Removing a Directory: Uses fs::remove to delete the directory. It only removes empty directories; to remove non-empty directories, use fs::remove_all.

Path Manipulation

Managing filesystem paths is a fundamental aspect of interacting with the filesystem. std::filesystem::path provides a robust way to handle paths, abstracting away platform-specific nuances.

Creating Paths

fs::path p1 = "folder/subfolder/file.txt";
fs::path p2 = "/absolute/path/to/file.txt";
fs::path p3("C:\\Program Files\\Application\\app.exe"); // Windows-style path

Concatenating Paths

Use the / operator to concatenate paths, ensuring correct separators are used based on the operating system.

fs::path base = "/home/user";
fs::path file = "document.txt";
fs::path fullPath = base / file; // "/home/user/document.txt"

std::cout << "Full Path: " << fullPath << std::endl;

Output:

Full Path: "/home/user/document.txt"

Extracting Components

std::filesystem::path allows easy extraction of various components of a path.

fs::path p = "/home/user/document.txt";

std::cout << "Root Name: " << p.root_name() << std::endl;       // "/"
std::cout << "Root Directory: " << p.root_directory() << std::endl; // "/"
std::cout << "Parent Path: " << p.parent_path() << std::endl;   // "/home/user"
std::cout << "Filename: " << p.filename() << std::endl;         // "document.txt"
std::cout << "Stem: " << p.stem() << std::endl;                 // "document"
std::cout << "Extension: " << p.extension() << std::endl;       // ".txt"

Output:

Root Name: "/"
Root Directory: "/"
Parent Path: "/home/user"
Filename: "document.txt"
Stem: "document"
Extension: ".txt"

Normalizing Paths

Use fs::canonical to resolve a path to its absolute, normalized form, eliminating redundant elements like . and ...

fs::path relativePath = "./folder/../file.txt";
try {
    fs::path absolutePath = fs::canonical(relativePath);
    std::cout << "Absolute Path: " << absolutePath << std::endl;
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error: " << e.what() << std::endl;
}

Output:

Absolute Path: "/current/working/directory/file.txt"

Note: fs::canonical requires that the path exists; otherwise, it throws an exception.


File Operations

Performing operations on files is a common requirement. std::filesystem provides various functions to create, copy, move, and delete files.

Creating a File

While std::filesystem doesn’t directly create files, you can use standard I/O streams in conjunction with it.

#include <filesystem>
#include <fstream>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path filePath = "example_dir/sample.txt";

    // Ensure the directory exists
    fs::create_directories(filePath.parent_path());

    // Create and write to the file
    std::ofstream ofs(filePath);
    if(ofs) {
        ofs << "Hello, Filesystem!";
        ofs.close();
        std::cout << "File created: " << filePath << std::endl;
    } else {
        std::cerr << "Failed to create file: " << filePath << std::endl;
    }

    return 0;
}

Output:

File created: "example_dir"/sample.txt

Copying Files

Use fs::copy to duplicate files.

fs::path source = "example_dir/sample.txt";
fs::path destination = "example_dir/sample_copy.txt";

try {
    fs::copy(source, destination, fs::copy_options::overwrite_existing);
    std::cout << "File copied to: " << destination << std::endl;
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error copying file: " << e.what() << std::endl;
}

Output:

File copied to: "example_dir"/sample_copy.txt

Moving and Renaming Files

Use fs::rename or fs::copy followed by fs::remove to move or rename files.

fs::path oldPath = "example_dir/sample_copy.txt";
fs::path newPath = "example_dir/renamed_sample.txt";

try {
    fs::rename(oldPath, newPath);
    std::cout << "File moved/renamed to: " << newPath << std::endl;
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error moving/renaming file: " << e.what() << std::endl;
}

Output:

File moved/renamed to: "example_dir"/renamed_sample.txt

Deleting Files

Use fs::remove to delete files.

fs::path fileToDelete = "example_dir/renamed_sample.txt";

try {
    if(fs::remove(fileToDelete)) {
        std::cout << "File deleted: " << fileToDelete << std::endl;
    } else {
        std::cout << "File not found: " << fileToDelete << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error deleting file: " << e.what() << std::endl;
}

Output:

File deleted: "example_dir"/renamed_sample.txt

Directory Operations

Managing directories is another critical aspect of filesystem interaction. std::filesystem offers functions to create, remove, rename, and query directories.

Creating Directories

Use fs::create_directory to create a single directory or fs::create_directories to create nested directories.

fs::path dirPath = "parent_dir/child_dir";

try {
    if(fs::create_directories(dirPath)) {
        std::cout << "Directories created: " << dirPath << std::endl;
    } else {
        std::cout << "Directories already exist: " << dirPath << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error creating directories: " << e.what() << std::endl;
}

Output:

Directories created: "parent_dir"/child_dir

Removing Directories

  • fs::remove: Removes a single empty directory.
  • fs::remove_all: Recursively removes a directory and all its contents.
fs::path emptyDir = "parent_dir/child_dir";
fs::path nonEmptyDir = "parent_dir";

// Remove empty directory
try {
    if(fs::remove(emptyDir)) {
        std::cout << "Empty directory removed: " << emptyDir << std::endl;
    } else {
        std::cout << "Empty directory not found: " << emptyDir << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error removing directory: " << e.what() << std::endl;
}

// Recursively remove non-empty directory
try {
    size_t removed = fs::remove_all(nonEmptyDir);
    std::cout << "Removed " << removed << " files/directories from: " << nonEmptyDir << std::endl;
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error removing directories recursively: " << e.what() << std::endl;
}

Output:

Empty directory removed: "parent_dir"/child_dir
Removed 1 files/directories from: "parent_dir"

Renaming Directories

Use fs::rename to rename or move directories.

fs::path oldDir = "old_directory";
fs::path newDir = "new_directory";

try {
    fs::rename(oldDir, newDir);
    std::cout << "Directory renamed to: " << newDir << std::endl;
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error renaming directory: " << e.what() << std::endl;
}

Output:

Directory renamed to: "new_directory"

Iterating Through Directories

Traversing directories allows you to access and manipulate their contents programmatically. std::filesystem provides iterators to facilitate this process.

Non-Recursive Iteration

Use fs::directory_iterator to iterate through the immediate contents of a directory without descending into subdirectories.

fs::path dir = "example_dir";

try {
    for(const auto& entry : fs::directory_iterator(dir)) {
        std::cout << entry.path() << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error iterating directory: " << e.what() << std::endl;
}

Output:

"example_dir"/file1.txt
"example_dir"/file2.txt
"example_dir"/subfolder

Recursive Iteration

Use fs::recursive_directory_iterator to iterate through a directory and all its subdirectories.

fs::path dir = "example_dir";

try {
    for(const auto& entry : fs::recursive_directory_iterator(dir)) {
        std::cout << entry.path() << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error iterating directories recursively: " << e.what() << std::endl;
}

Output:

"example_dir"/file1.txt
"example_dir"/file2.txt
"example_dir"/subfolder
"example_dir"/subfolder/file3.txt

Filtering Entries

Combine iterators with conditional checks to filter specific types of entries (e.g., regular files, directories).

fs::path dir = "example_dir";

try {
    for(const auto& entry : fs::directory_iterator(dir)) {
        if(fs::is_regular_file(entry.status())) {
            std::cout << "File: " << entry.path() << std::endl;
        } else if(fs::is_directory(entry.status())) {
            std::cout << "Directory: " << entry.path() << std::endl;
        }
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error iterating directory with filters: " << e.what() << std::endl;
}

Output:

File: "example_dir"/file1.txt
File: "example_dir"/file2.txt
Directory: "example_dir"/subfolder

Querying File Properties

Accessing file metadata is crucial for various applications, such as determining file sizes, modification times, and permissions.

Common File Properties

  1. File Size: The size of the file in bytes.
  2. File Status: Information about the file type, permissions, and more.
  3. Last Write Time: The timestamp of the last modification.
  4. File Permissions: Read, write, and execute permissions.

Examples

Getting File Size

fs::path filePath = "example_dir/file1.txt";

try {
    auto size = fs::file_size(filePath);
    std::cout << "Size of " << filePath << ": " << size << " bytes" << std::endl;
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error getting file size: " << e.what() << std::endl;
}

Output:

Size of "example_dir"/file1.txt: 1024 bytes

Checking File Status

fs::path filePath = "example_dir/file1.txt";

try {
    fs::file_status status = fs::status(filePath);

    if(fs::is_regular_file(status)) {
        std::cout << filePath << " is a regular file." << std::endl;
    } else if(fs::is_directory(status)) {
        std::cout << filePath << " is a directory." << std::endl;
    } else {
        std::cout << filePath << " is neither a regular file nor a directory." << std::endl;
    }

    // Checking permissions
    if(fs::status_known(status)) {
        std::cout << "Permissions: " << fs::status(status).permissions() << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error checking file status: " << e.what() << std::endl;
}

Output:

"example_dir"/file1.txt is a regular file.
Permissions: 0o644

Note: The permissions output format may vary based on the operating system.

Retrieving Last Write Time

fs::path filePath = "example_dir/file1.txt";

try {
    auto ftime = fs::last_write_time(filePath);
    auto sctp = decltype(ftime)::clock::to_sys(ftime);
    std::time_t cftime = system_clock::to_time_t(sctp);
    std::cout << "Last write time: " << std::ctime(&cftime);
} catch(const fs::filesystem_error& e) {
    std::cerr << "Error retrieving last write time: " << e.what() << std::endl;
}

Output:

Last write time: Wed Oct 11 14:23:45 2023

Explanation:

  • fs::last_write_time: Retrieves the last modification time as a file_time_type.
  • Conversion: Converts file_time_type to std::time_t for human-readable output.

Error Handling in Filesystem Operations

Filesystem operations can fail due to various reasons, such as insufficient permissions, non-existent paths, or I/O errors. Proper error handling ensures that your application can gracefully handle such scenarios.

Exceptions vs. Error Codes

  • Exceptions: Most std::filesystem functions throw std::filesystem::filesystem_error exceptions upon failure.
  • Error Codes: Some functions offer overloads that accept an std::error_code parameter to report errors without throwing exceptions.

Using Try-Catch Blocks

Encapsulate filesystem operations within try-catch blocks to handle exceptions.

fs::path filePath = "example_dir/nonexistent.txt";

try {
    if(fs::exists(filePath)) {
        std::cout << filePath << " exists." << std::endl;
    } else {
        std::cout << filePath << " does not exist." << std::endl;
    }
} catch(const fs::filesystem_error& e) {
    std::cerr << "Filesystem error: " << e.what() << std::endl;
}

Output:

"example_dir"/nonexistent.txt does not exist.

Using Overloads with std::error_code

Avoid exceptions by using function overloads that accept an std::error_code.

fs::path filePath = "example_dir/sample.txt";
std::error_code ec;

bool exists = fs::exists(filePath, ec);
if(ec) {
    std::cerr << "Error checking existence: " << ec.message() << std::endl;
} else {
    std::cout << filePath << (exists ? " exists." : " does not exist.") << std::endl;
}

Output:

"example_dir"/sample.txt exists.

Advantages:

  • Performance: Avoids the overhead associated with exceptions.
  • Control: Gives more granular control over error handling.

Disadvantages:

  • Verbose: Requires explicit error checking after each operation.

Best Practices

  1. Choose the Right Error Handling Strategy: Use exceptions for critical errors that cannot be recovered from and error codes for recoverable or expected errors.
  2. Provide Meaningful Messages: When handling errors, provide clear and informative messages to aid in debugging.
  3. Clean Up Resources: Ensure that resources are properly released even when errors occur, leveraging RAII where possible.
  4. Validate Paths: Before performing operations, validate that paths meet the expected criteria to minimize errors.

Best Practices with std::filesystem

Adhering to best practices ensures that your use of std::filesystem is efficient, safe, and maintainable.

  1. Use std::filesystem::path Over Raw Strings

    • Advantages: Handles platform-specific path separators, provides rich functionality for path manipulation.
    • Example:
      fs::path p = "folder/subfolder/file.txt";
      
  2. Leverage RAII for Resource Management

    • Ensure that resources like file handles are properly managed using RAII principles to prevent leaks.
    • Example:
      std::ofstream ofs(filePath); // Automatically closes when going out of scope
      
  3. Prefer Non-Throwing Overloads with std::error_code When Appropriate

    • Use non-throwing overloads in performance-critical sections or where exceptions are undesirable.
    • Example:
      std::error_code ec;
      fs::remove(filePath, ec);
      if(ec) { /* handle error */ }
      
  4. Check for Path Existence Before Operations

    • Prevent unnecessary errors by verifying that paths exist before attempting operations.
    • Example:
      if(fs::exists(filePath)) {
          // Proceed with operation
      }
      
  5. Handle Permissions Appropriately

    • Be aware of file and directory permissions, especially when creating or modifying them.
    • Example:
      fs::permissions(filePath, fs::perms::owner_all, fs::perm_options::add);
      
  6. Use Iterators for Efficient Directory Traversal

    • Utilize directory_iterator and recursive_directory_iterator for efficient and readable directory traversal.
    • Example:
      for(auto& entry : fs::directory_iterator(dirPath)) {
          // Process entry
      }
      
  7. Normalize Paths When Necessary

    • Use functions like fs::canonical to resolve and normalize paths, ensuring consistency.
    • Example:
      fs::path normalized = fs::canonical(p);
      
  8. Be Mindful of Platform Differences

    • Understand that certain filesystem behaviors may vary across operating systems (e.g., case sensitivity on Windows vs. Linux).
  9. Keep Code Clean and Readable

    • Avoid overly complex path manipulations; break down operations into manageable steps for clarity.
  10. Stay Updated with Compiler Support

    • Ensure that your development environment fully supports the features of std::filesystem you intend to use, as implementations may vary across different compilers.

Practical Examples

To reinforce your understanding of std::filesystem, let’s explore several practical examples that demonstrate its capabilities in real-world scenarios.

Example 1: Listing All Files in a Directory

Problem: Create a program that lists all files (excluding directories) in a specified directory.

Solution:

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path dirPath = "example_dir";

    try {
        if(!fs::exists(dirPath) || !fs::is_directory(dirPath)) {
            std::cerr << dirPath << " is not a valid directory." << std::endl;
            return 1;
        }

        std::cout << "Files in " << dirPath << ":" << std::endl;
        for(const auto& entry : fs::directory_iterator(dirPath)) {
            if(fs::is_regular_file(entry.status())) {
                std::cout << " - " << entry.path().filename() << std::endl;
            }
        }
    } catch(const fs::filesystem_error& e) {
        std::cerr << "Filesystem error: " << e.what() << std::endl;
    }

    return 0;
}

Output:

Files in "example_dir":
 - file1.txt
 - file2.txt
 - image.png

Explanation:

  • Directory Validation: Checks if the specified path exists and is a directory.
  • Iterating Entries: Uses directory_iterator to traverse immediate entries.
  • Filtering Files: Filters out directories, listing only regular files.

Example 2: Recursively Searching for a File

Problem: Implement a function that searches for a file with a given name within a directory and all its subdirectories.

Solution:

#include <filesystem>
#include <iostream>
#include <string>

namespace fs = std::filesystem;

// Function to search for a file recursively
fs::path findFile(const fs::path& directory, const std::string& filename) {
    try {
        for(const auto& entry : fs::recursive_directory_iterator(directory)) {
            if(fs::is_regular_file(entry.status()) && entry.path().filename() == filename) {
                return entry.path();
            }
        }
    } catch(const fs::filesystem_error& e) {
        std::cerr << "Error during search: " << e.what() << std::endl;
    }
    return fs::path(); // Return empty path if not found
}

int main() {
    fs::path searchDir = "example_dir";
    std::string targetFile = "document.pdf";

    fs::path result = findFile(searchDir, targetFile);

    if(!result.empty()) {
        std::cout << "File found at: " << result << std::endl;
    } else {
        std::cout << "File " << targetFile << " not found in " << searchDir << std::endl;
    }

    return 0;
}

Output:

File found at: "example_dir"/subfolder/documents/document.pdf

Explanation:

  • Recursive Iteration: Utilizes recursive_directory_iterator to traverse all subdirectories.
  • Filename Matching: Compares the target filename with each entry’s filename.
  • Result Handling: Returns the path if found; otherwise, returns an empty path.

Example 3: Copying a Directory

Problem: Develop a program that copies the contents of one directory to another, preserving the directory structure.

Solution:

#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

int main() {
    fs::path sourceDir = "source_directory";
    fs::path destinationDir = "destination_directory";

    try {
        // Check if source directory exists
        if(!fs::exists(sourceDir) || !fs::is_directory(sourceDir)) {
            std::cerr << "Source directory does not exist or is not a directory." << std::endl;
            return 1;
        }

        // Create destination directory if it doesn't exist
        if(!fs::exists(destinationDir)) {
            fs::create_directories(destinationDir);
            std::cout << "Created destination directory: " << destinationDir << std::endl;
        }

        // Iterate through source directory recursively
        for(auto& entry : fs::recursive_directory_iterator(sourceDir)) {
            const auto& path = entry.path();
            auto relativePath = fs::relative(path, sourceDir);
            fs::path destPath = destinationDir / relativePath;

            if(fs::is_directory(entry.status())) {
                fs::create_directories(destPath);
            } else if(fs::is_regular_file(entry.status())) {
                fs::copy_file(path, destPath, fs::copy_options::overwrite_existing);
                std::cout << "Copied file: " << path << " to " << destPath << std::endl;
            }
        }

        std::cout << "Directory copy completed successfully." << std::endl;
    } catch(const fs::filesystem_error& e) {
        std::cerr << "Filesystem error during copy: " << e.what() << std::endl;
    }

    return 0;
}

Output:

Created destination directory: "destination_directory"
Copied file: "source_directory"/file1.txt to "destination_directory"/file1.txt
Copied file: "source_directory"/file2.txt to "destination_directory"/file2.txt
Copied file: "source_directory"/subfolder/documents/document.pdf to "destination_directory"/subfolder/documents/document.pdf
Directory copy completed successfully.

Explanation:

  • Relative Paths: Uses fs::relative to maintain the directory structure in the destination.
  • Directory Creation: Creates directories in the destination as needed.
  • File Copying: Copies regular files, overwriting existing ones if necessary.

Summary

The std::filesystem library in C++20 provides a comprehensive and standardized way to interact with the filesystem. By leveraging its features, you can perform a wide range of file and directory operations in a portable and efficient manner. Key takeaways from this chapter include:

  • Path Management: Utilize std::filesystem::path for robust and platform-independent path handling.
  • File and Directory Operations: Perform creation, deletion, copying, moving, and renaming of files and directories with ease.
  • Iterators: Traverse directories efficiently using directory_iterator and recursive_directory_iterator.
  • File Properties: Access and manipulate file metadata, such as size, permissions, and modification times.
  • Error Handling: Implement robust error handling strategies using exceptions or error codes to manage filesystem-related errors gracefully.
  • Best Practices: Follow recommended practices to write clean, efficient, and maintainable filesystem code.

By mastering std::filesystem, you equip yourself with the tools to handle one of the most fundamental aspects of software development: interacting with the file system. Whether you’re building simple utilities, complex applications, or managing resources, std::filesystem offers the capabilities needed to perform these tasks effectively.


Next, you’ll move on to the following chapter Best Practices and Design Patterns, where you’ll explore modern C++ idioms, design patterns, and strategies to write efficient and maintainable code.

Next chapter: Best Practices and Design Patterns