Skip to content

Latest commit

 

History

History
2179 lines (1915 loc) · 91.3 KB

ch20.md

File metadata and controls

2179 lines (1915 loc) · 91.3 KB

Chapter20 文件系统库

直到C++17,Boost.Filesystem库终于被C++标准采纳。 在这个过程中,这个库用新的语言特性进行了很多调整、改进了和其他库的一致性、 进行了精简、还扩展了很多缺失的功能(例如计算两个文件系统路径之间的相对路径)。

20.1 基本的示例

让我们以一些基本的示例开始。

20.1.1 打印文件系统路径类的属性

下面的程序允许我们传递一个字符串作为文件系统路径,然后根据给定路径的文件类型打印出一些信息:

#include <iostream>
#include <filesystem>
#include <cstdlib> // for EXIT_FAILURE

int main(int argc, char* argv[])
{
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " <path> \n";
        return EXIT_FAILURE;
    }

    std::filesystem::path p{argv[1]};   // p代表一个文件系统路径(可能不存在)
    if (is_regular_file(p)) {           // 路径p是普通文件吗?
        std::cout << p << " exists with " << file_size(p) << " bytes\n";
    }
    else if (is_directory(p)) {         // 路径p是目录吗?
        std::cout << p << " is a directory containing:\n";
        for (const auto& e : std::filesystem::directory_iterator{p}) {
            std::cout << "  " << e.path() << '\n';
        }
    }
    else if (exists(p)) {               // 路径p存在吗?
        std::cout << p << " is a special file\n";
    }
    else {
        std::cout << "path " << p << " does not exist\n";
    }
}

我们首先把传入的命令行参数转换为了一个文件系统路径:

std::filesystem::path p{argv[1]};   // p代表一个文件系统路径(有可能不存在)

然后,我们进行了下列检查:

  • 如果该路径代表一个普通文件,我们打印出它的大小:
if (is_regular_file(p)) {   // 路径p是普通文件吗?
    std::cout << p << " exists with " << file_size(p) << " bytes\n";
}

像下面这样调用程序:

checkpath checkpath.cpp

将会有如下输出:

"checkpath.cpp" exists with 907 bytes

注意输出路径时会自动把路径名用双引号括起来输出 (把路径用双引号括起来、反斜杠用另一个反斜杠转义, 对Windows路径来说是一个问题)。

  • 如果路径是一个目录,我们遍历这个目录中的所有文件并打印出这些文件的路径:
if (is_directory(p)) {      // 路径p是目录吗?
    std::cout << p << " is a directory containing:\n";
    for (auto& e : std::filesystem::directory_iterator{p}) {
        std::cout << "  " << e.path() << '\n';
    }
}

这里,我们使用了directory_iterator, 它提供了begin()end(),所以我们可以使用范围for循环来遍历 directory_entry元素。在这里,我们使用了directory_entry的 成员函数path(),返回该目录项的文件系统路径。 像下面这样调用程序:

checkpath .

输出将是:

"." is a directory containing:
"./checkpath.cpp"
"./checkpath.exe"
...
  • 最后,我们检查传入的文件系统路径是否不存在:
if (exists(p)) {        // 路径p存在吗?
    ...
}

注意根据 参数依赖查找(argument dependent lookup)(ADL) , 你不需要使用完全限定的名称来调用is_regular_ file()file_size()is_directory()exists()等函数。 它们都属于命名空间std::filesystem,但是因为它们的参数也属于这个命名空间, 所以调用它们时会自动在这个命名空间中进行查找。

在Windows下处理路径

(译者注:可能是水平有限,完全看不懂作者在这一小节的逻辑, 所以只能按照自己的理解胡乱翻译,如有错误请见谅。)

默认情况下,输出路径时用双引号括起来并用反斜杠转义反斜杠在Windows 下会导致一个问题。在Windows下以如下方式调用程序:

checkpath C:\

将会有如下输出:

"C:\\" is a directory containing:
...
"C:\\Users"
"C:\\Windows"

用双引号括起来输出路径可以确保输出的文件名可以被直接复制粘贴到其他程序里, 并且经过转义之后还会恢复为原本的文件名。 然而,终端通常不接受这样的路径。

因此,一个在Windows下的可移植版本应该使用成员函数string(), 这样可以在向标准输出写入路径时避免输出双引号:

#include <iostream>
#include <filesystem>
#include <cstdlib>  // for EXIT_FAILURE

int main(int argc, char* argv[])
{
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " <path> \n";
        return EXIT_FAILURE;
    }
    std::filesystem::path p{argv[1]};   // p代表一个文件系统路径(可能不存在)
    if (is_regular_file(p)) {           // 路径p是普通文件吗?
        std::cout << '"' << p.string() << "\" existes with " << file_size(p) << " bytes\n";
    }
    else if (is_directory(p)) {         // 路径p是目录吗?
        std::cout << '"' << p.string() << "\" is a directory containing:\n";
        for (const auto& e : std::filesystem::directory_iterator{p}) {
            std::cout << "  \"" << e.path().string() << "\"\n";
        }
    }
    else if (exists(p)) {               // 路径p存在吗?
        std::cout << '"' << p.string() << "\" is a special file\n";
    }
    else {
        std::cout << "path \"" << p.string() << "\" does not exist\n";
    }
}

现在,在Windows上以如下方式调用程序:

checkpath C:\

将会有如下输出:

"C:\" is a directory containing:
...
"C:\Users"
"C:\Windows"

有一些其他转换可以把路径转换为通用格式 或者把string转换为本地编码。

20.1.2 用switch语句处理不同的文件系统类型

我们可以像下面这样修改并改进上面的例子:

#include <iostream>
#include <filesystem>
#include <cstdlib>  // for EXIT_FAILURE

int main(int argc, char* argv[])
{
    if (argc < 2) {
        std::cout << "Usage: " << argv[0] << " <path> \n";
        return EXIT_FAILURE;
    }
    namespace fs = std::filesystem;

    switch (fs::path p{argv[1]}; status(p).type()) {
        case fs::file_type::not_found:
            std::cout << "path \"" << p.string() << "\" does not exist\n";
            break;
        case fs::file_type::regular:
            std::cout << '"' << p.string() << "\" exists with " << file_size(p) << " bytes\n";
            break;
        case fs::file_type::directory:
            std::cout << '"' << p.string() << "\" is a directory containing:\n";
            for (const auto& e : std::filesystem::directory_iterator{p}) {
                std::cout << "  " << e.path().string() << '\n';
            }
            break;
        default:
            std::cout << '"' << p.string() << "\" is a special file\n";
            break;
    }
}

命名空间fs

首先,我们做了一个非常普遍的操作:我们定义了fs作为命名空间 std::filesystem的缩写:

namespace fs = std::filesystem;

使用这个新初始化的命名空间的一个例子是下面switch语句中的路径p

fs::path p{argv[1]};

这里的switch语句使用了新的带初始化的switch语句特性, 初始化路径的同时把路径的类型作为分支条件:

switch (fs::path p{argv[1]}; status(p).type()) {
    ...
}

表达式status(p).type()首先创建了一个file_status对象, 然后该对象的type()方法返回了一个file_type类型的值。 通过这种方式我们可以直接处理不同的类型,而不需要使用is_regular_file()is_directory()等函数构成的if-else链。 我们通过多个步骤(先调用status()再调用type())才得到了最后的类型, 因此我们不需要为不感兴趣的其他信息付出多余的系统调用开销。

注意可能已经有特定实现的file_type存在。例如, Windows就提供了特殊的文件类型junction。 然而,使用了它的代码是不可移植的。

20.1.3 创建不同类型的文件

在介绍了文件系统的只读操作之后,让我们给出首个进行修改的例子。 下面的程序在一个子目录tmp中创建了不同类型的文件:

#include <iostream>
#include <fstream>
#include <filesystem>
#include <cstdlib> // for std::exit()和EXIT_FAILURE

int main()
{
    namespace fs = std::filesystem;
    try {
        // 创建目录tmp/test/(如果不存在的话):
        fs::path testDir{"tmp/test"};
        create_directories(testDir);

        // 创建数据文件tmp/test/data.txt:
        auto testFile = testDir / "data.txt";
        std::ofstream  dataFile{testFile};
        if (!dataFile) {
            std::cerr << "OOPS, can't open \"" << testFile.string() << "\"\n";
            std::exit(EXIT_FAILURE);    // 失败退出程序
        }
        dataFile << "The answer is 42\n";

        // 创建符号链接tmp/slink/,指向tmp/test/:
        create_directory_symlink("test", testDir.parent_path() / "slink");
    }
    catch (const fs::filesystem_error& e) {
        std::cerr << "EXCEPTION: " << e.what() << '\n';
        std::cerr << "    path1: \"" << e.path1().string() << "\"\n";
    }

    // 递归列出所有文件(同时遍历符号链接):
    std::cout << fs::current_path().string() << ":\n";
    auto iterOpts{fs::directory_options::follow_directory_symlink};
    for (const auto& e : fs::recursive_directory_iterator(".", iterOpts)) {
        std::cout << "  " << e.path().lexically_normal().string() << '\n';
    }
}

让我们一步步来分析这段程序。

命名空间fs

首先,我们又一次定义了fs作为命名空间std::filesystem的缩写:

namespace fs = std::filesystem;

之后我们使用这个命名空间为临时文件创建了一个基本的子目录:

fs::path testDir{"tmp/test"};

创建目录

当我们尝试创建子目录时:

create_directories(testDir);

通过使用create_directories()我们可以递归创建整个路径中所有缺少的目录 (还有一个create_ directory()只在已存在的目录中创建目录)。

当目标目录已经存在时这个调用并不会返回错误。 然而,其他的问题会导致错误并抛出一个相应的异常。

如果testDir已经存在,create_directories()会返回false。 因此,你可以这么写:

if (!create_directories(testDir)) {
    std::cout << "\"" << testDir.string() << "\" already exists\n";
}

创建普通文件

之后我们用一些内容创建了一个新文件tmp/test/data.txt

auto testFile = testDir / "data.txt";
std::ofstream dataFile{testFile};
if (!dataFile) {
    std::cerr << "OOPS, can't open \"" << testFile.string() << "\"\n";
    std::exit(EXIT_FAILURE);  // 失败退出程序
}
dataFile << "The answer is 42\n";

这里,我们使用了运算符/来扩展路径,然后传递给文件流的构造函数。 如你所见,普通文件的创建可以使用现有的I/O流库来实现。 然而,I/O流的构造函数多了一个以文件系统路径为参数的版本 (一些函数例如open()也添加了这种重载版本)。

注意你仍然应该总是检查创建/打开文件的操作是否成功了。 这里有很多种可能发生的错误(见下文)。

创建符号链接

接下来的语句尝试创建符号链接tmp/slink指向目录tmp/test

create_directory_symlink("test", testDir.parent_path() / "slink");

注意第一个参数的路径是以即将创建的符号链接所在的目录为起点的相对路径。 因此,你必须传递"test"而不是"tmp/test"来高效的 创建链接tmp/slink指向tmp/test。如果你调用:

std::filesystem::create_directory_symlink("tmp/test", "tmp/slink");

你将会高效的创建符号链接tmp/slink,然而它会指向tmp/tmp/test

注意通常情况下,也可以调用create_symlink()代替create_ directory_symlink()来创建目录的符号链接。 然而,一些操作系统可能对目录的符号链接有特殊处理或者当知道要创建的符号链接指向目录时会有优化, 因此,当你想创建指向目录的符号链接时你应该使用create_directory_symlink()

最后,注意这个调用在Windows上可能会失败并导致错误处理, 因为创建符号链接可能需要管理员权限。

递归遍历目录

最后,我们递归地遍历了当前目录:

auto iterOpts = fs::directory_options::follow_directory_symlink;
for (auto& e : fs::recursive_directory_iterator(".", iterOpts)) {
    std::cout << "  " << e.path().lexically_normal().string() << '\n';
}

注意我们使用了一个递归目录迭代器并传递了选项follow_directory_symlink来 遍历符号链接。因此,我们在POSIX兼容系统上可能会得到类似于如下输出:

/home/nico:
...
tmp
tmp/slink
tmp/slink/data.txt
tmp/test
tmp/test/data.txt
...

在Windows系统上有类似如下输出:

C:\Users\nico:
...
tmp
tmp\slink
tmp\slink\data.txt
tmp\test
tmp\test\data.txt
...

注意在我们打印目录项之前调用了lexically_normal()。 如果略过这一步,目录项的路径可能会包含一个前缀, 这个前缀是创建目录迭代器时传递的实参。 因此,在循环内直接打印路径:

auto iterOpts = fs::directory_options::follow_directory_symlink;
for (auto& e : fs::recursive_directory_iterator(".", iterOpts)) {
    std::cout << "  " << e.path() << '\n';
}

将会在POSIX兼容系统上有如下输出:

all files:
...
"./testdir"
"./testdir/data.txt"
"./tmp"
"./tmp/test"
"./tmp/test/data.txt"

在Windows上,输出将是:

all files:
...
".\\testdir"
".\\testdirdata.txt"
".\\tmp"
".\\tmptest"
".\\tmptestdata.txt"

通过调用lexically_normal()我们可以得到正规化的路径, 它移除了前导的代表当前路径的点。还有,如上文所述, 通过调用string()我们避免了输出路径时用双引号括起来。 这里没有调用string()的输出结果在POSIX兼容的系统上看起来OK(只是路径两端有双引号), 但在Windows上的结果看起来就很奇怪(因为每一个反斜杠都需要反斜杠转义)。

错误处理

文件系统往往是麻烦的根源。你可能因为在文件名中使用了无效的字符而导致操作失败, 或者当你正在访问文件系统时它已经被其他程序修改了。 因此,根据平台和权限的不同,这个程序中可能会有很多问题。

对于那些没有被返回值覆盖的情况(例如当目录已经存在时), 我们捕获了相应的异常并打印了一般的信息和第一个路径:

try {
    ...
}
catch (const fs::filesystem_error& e) {
    std::cerr << "EXCEPTION: " << e.what() << '\n';
    std::cerr << "    path1: \"" << e.path1().string() << "\"\n";
}

例如,如果我们不能创建目录,将会打印出类似于如下消息:

EXCEPTION: filesystem error: cannot create directory: [tmp/test]
path1: "tmp/test"

如果我们不能创建符号链接可能是因为它已经存在了, 或者我们可能需要特殊权限,这些情况下你可能会得到如下消息:

EXCEPTION: create_directory_symlink: Can't create a file when it already exists:
                                     "tmp\test\data.txt", "testdir"
    path1: "tmp\test\data.txt"

或者:

EXCEPTION: create_directory_symlink: A requied privilege is not held by the
                                     client.: "test", "tmp\slink"
    path1: "test"

在每一种情况下,都要注意在多用户/多进程操作系统中情况可能会在任何时候改变, 这意味着你刚刚创建的目录甚至可能已经被删除、重命名、或已经被同名的文件覆盖。 因此,很显然不能只根据当前的情况就保证一个预期操作一定是有效的。 最好的方式就是尝试做想做的操作(例如,创建目录、打开文件) 并处理抛出的异常和错误,或者验证预期的行为。

然而,有些时候文件系统操作能正常执行但不是按你预想的结果。 例如,如果你想在指定目录中创建一个文件并且已经有了一个和目录同名的指向另一个目录的符号链接, 那么这个文件可能在一个预料之外的地方创建或者覆写。

这种情况是有可能的(用户完全有可能会创建目录的符号链接),但是如果你想检测这种情况, 在创建文件之前你需要检查文件是否存在(这可能比你一开始想的要复杂很多)。

再强调一次:文件系统并不保证进行处理之前的检查的结果直到你进行处理时仍然有效。

20.1.4 使用并行算法处理文件系统

参见dirsize.cpp查看另一个使用并行算法计算目录树中所有文件大小之和的例子。

20.2 原则和术语

在讨论文件系统库的细节之前,我们不得不继续介绍一些设计原则和术语。 这是必须的,因为标准库要覆盖不同的操作系统并把系统提供的接口映射为公共的API。

20.2.1 通用的可移植的分隔符

C++标准库不仅标准化了所有操作系统的文件系统中公共的部分,在很多情况下, C++标准还尽可能的遵循POSIX标准的要求来实现。 对于一些操作,只要是合理的就应该能正确执行,如果操作是不合理的,实现应该报错。 这些错误可能是:

  • 特殊的字符不能被用作文件名
  • 创建了文件系统不支持的元素(例如,符号链接)

不同文件系统的差异也应该纳入考虑:

  • 大小写敏感: "hello.txt""Hello.txt""hello.TXT"可能指向 同一个文件(Windows上)也可能指向三个不同的文件(POSIX兼容系统)。
  • 绝对路径和相对路径: 在某些系统上,"/bin"是一个绝对路径(POSIX兼容系统), 然而在某些系统上不是(Windows)。

20.2.2 命名空间

文件系统库在std里有自己的子命名空间filesystem。 一个很常见的操作是定义缩写fs

namespace fs = std::filesystem;

这允许我们使用fs::current_path()代 替std::filesystem::current_path()

这一章的示例代码中将经常使用fs作为缩写。

注意你应该总是使用完全限定的函数调用,尽管不指明命名空间时 通过 参数依赖查找(argument dependent lookup)(ADL) 也能够工作。 但如果不用命名空间限定有时可能导致意外的行为。

20.2.3 文件系统路径

文件系统库的一个关键元素是path。它代表文件系统中某一个文件的位置。 它由可选的根名称、可选的根目录、和一些以目录分隔符分隔的文件名组成。 路径可以是相对的(此时文件的位置依赖于当前的工作目录)或者是绝对的。

路径可能有不同的格式:

  • 通用格式,这是可移植的
  • 本地格式,这是底层文件系统特定的

在POSIX兼容系统上通用格式和本地格式没有什么区别。 在Windows上,通用格式/tmp/test.txt也是有效的本地格式, 另外\tmp\test.txt也是有效的 (/tmp/test.txt\tmp\test.txt 是同一个路径的两种本地版本)。在OPenVMS上,相应的本地格式将是[tmp]test.txt

也有一些特殊的文件名:

  • "."代表当前目录
  • ".."代表父目录

通用的路径格式如下:

[rootname] [rootdir] [relativepath]

这里:

  • 可选的根名称是实现特定的(例如,在POSIX系统上可以是//host, 而在Windows上可以是C:
  • 可选的根目录是一个目录分隔符
  • 相对路径是若干目录分隔符分隔的文件名

目录分隔符由一个或多个'/'组成或者是实现特定的。

可移植的通用路径的例子有:

//host1/bin/hello.txt
.
tmp/
/a/b//.../c

注意在POSIX系统上最后一个路径和/a/c指向同一个位置,并且都是绝对路径。 而在Windows上则是相对路径(因为没有指定驱动器/分区(盘))。

另一方面,C:/bin在Windows上是绝对路径(在"C"盘上的根目录"bin"), 但在POSIX系统上是一个相对路径(目录"C:"下的子目录"bin")。

在Windows系统上,反斜杠是实现特定的目录分隔符, 因此上面的路径 可以使用反斜杠作为目录分隔符:

host1\bin\hello.txt
.
tmp\
\a\b\..\c

文件系统库提供了在本地格式和通用格式之间转换的函数。

一个path可能为空,这意味着没有定义路径。 这种状态的含义 需要和"."一样。它的含义依赖于上下文。

20.2.4 正规化

路径可以进行正规化,在正规化的路径中:

  • 文件名由单个推荐的目录分隔符分隔。
  • 除非整个路径就是"."(代表当前目录),否则路径中不会使用"."
  • 路径中除了开头以外的地方不会包含".."(不能在路径中上下徘徊)。
  • 除非整个路径就是"."或者"..",否则当路径结尾的文件名是目录时要在最后加上目录分隔符。

注意正规化之后以目录分隔符结尾的路径和不以目录分隔符结尾的路径是不同的。 这是因为在某些操作系统中,当它们知道目标路径是一个目录时行为可能会发生改变 (例如,有尾部的分隔符时符号链接将被解析)。

表路径正规化的效果列举了一些在POSIX系统和Windows系统上 对路径进行正规化的例子。注意再重复一次,在POSIX系统上,C:barC:只是 把冒号作为文件名的一部分的单个文件名,并没有特殊的含义,而在Windows上,它们指定了一个分区。

路径 POSIX正规化 Windows正规化
foo/.///bar/../ foo/ foo\
//host/../foo.txt //host/foo.txt \\host\foo.txt
./f/../.f/ .f/ .f\
C:bar/../ . C:
C:/bar/.. C:/ C:\
C:\bar\.. C:\bar\.. C:\
/./../data.txt /data.txt \data.txt
././ . .

注意路径C:\bar\..在POSIX兼容系统上经过正规化 之后没有任何变化。原因是这些系统上反斜杠并不是目录分隔符,所以这整个路径只是一个 带有冒号、两个反斜杠、两个点的 单个 文件名。

文件系统库同时提供了词法正规化(不访问文件系统) 和依赖文件系统的正规化两种方式的相关函数。

20.2.5 成员函数VS独立函数

文件系统库提供了一些函数,有些是成员函数有些是独立函数。这么做的目的是:

  • 成员函数开销较小 。这是因为它们是纯词法的操作,并不会访问实际的文件系统, 这意味着它们不需要进行操作系统调用。例如:
mypath.is_absolute()        // 检查路径是否是绝对的
  • 独立函数开销较大 。因为它们通常会访问实际的文件系统, 这意味着需要进行操作系统调用。例如:
equivalent(path1, path2);   // 如果两个路径指向同一个文件则返回true

有时,文件系统库甚至为同一个功能既提供根据词法的版本又提供访问实际文件系统的版本:

std::filesystem::path fromP, toP;
...
toP.lexically_relative(fromP);  // 返回从fromP到toP的词法路径
relative(toP, fromP);           // 返回从fromP到toP的实际路径

得益于 参数依赖查找(ADL) ,很多情况下当调用独立函数时你不需要指明完整命名空间 std::filesystem,只要参数是文件系统库里定义的类型。只有当用其它类型隐式转换 为参数时你才需要给出完全限定的函数名。例如:

create_directory(std::filesystem::path{"tmpdir"});  // OK
remove(std::filesystem::path{"tmpdir"});            // OK
std::filesystem::create_directory("tmpdir");        // OK
std::filesystem::remove("tmpdir");                  // OK
create_directory("tmpdir");                         // ERROR

最后一个调用将会编译失败,因为我们并没有传递文件系统命名空间里的类型作为参数, 因此也不会在该命名空间里查找符号create_directory

然而,这里有一个著名的陷阱:

remove("tmpdir");   // OOPS:调用C函数remove()

根据你包含的头文件,这个调用可能会找到C函数remove(), 它的行为有一些不同:它也会删除指定的文件但不会删除空目录。

因此,强烈推荐使用完全限定的文件系统库里的函数名。例如:

namespace fs = std::filesystem;
...
fs::remove("tmpdir");   // OK:调用C++文件系统库函数remove()

20.2.6 错误处理

如上文所述,文件系统是错误的根源。 你必须考虑相应的文件是否存在、文件操作是否被允许、该操作是否会违背资源限制。 另外,当程序运行时其它进程可能创建、修改、或者移除了某些文件,这意味着事先检查 并不能保证没有错误。

问题在于从理论上讲,你不能提前保证下一次文件系统操作能够成功。 任何事先检查的结果都可能在你实际进行处理时失效。 因此,最好的方法是在进行一个或多个文件系统操作时处理好相应的异常或者错误。

注意,当读写普通文件时,默认情况下I/O流并不会抛出异常或错误。 当操作遇到错误时它只会什么也不做。因此,建议至少检查一下文件是否被成功打开。

因为并不是所有情况下都适合抛出异常(例如当一个文件系统调用失败时你想直接处理), 所以文件系统库使用了混合的异常处理方式:

  • 默认情况下,文件系统错误会作为异常处理。
  • 然而,如果你想的话可以在本地处理具体的某一个错误。

因此,文件系统库通常为每个操作提供两个重载版本:

  • 默认情况下(没有额外的错误处理参数),出现错误时抛出filesystem_error异常。
  • 传递额外的输出参数时,可以得到一个错误码或错误信息,而不是异常。

注意在第二种情况下,你可能会得到一个特殊的返回值来表示特定的错误。

使用filesystem_error异常

例如,你可以尝试像下面这样创建一个目录:

if (!create_directory(p)) { // 发生错误时抛出异常(除非错误是该路径已经存在)
    std::cout << p << " already exists\n";  // 该路径已经存在
}

这里没有传递错误码参数,因此错误时通常会抛出异常。 然而,注意当目录已存在时这种特殊情况是直接返回false。 因此,只有当其他错误例如没有权限创建目录、路径p无效、 违反了文件系统限制(例如路径长度超过上限)时才会抛出异常。

可以直接或间接的用try-catch包含这段代码, 然后处理std::filesystem::filesystem_error异常:

try {
    ...
    if (!create_directory(p)) { // 错误时抛出异常(除非错误是该路径已经存在)
        std::cout << p << " already exists\n"; // 该路径已经存在
    }
    ...
}
catch (const std::filesystem::filesystem_error& e) { // 派生自std::exception
    std::cout << "EXCEPTION: " << e.what() << '\n';
    std::cout << "     path: " << e.path1() << '\n';
}

如你所见,文件系统异常提供了标准异常的what()函数API来返回一个 实现特定的错误信息。然而,API还提供了path1()来获取错误相关的第一个路径, 和path2()来获取相关的第二个路径。

使用error_code参数

另一种创建目录的方式如下所示:

std::error_code ec;
create_directory(p, ec);    // 发生错误时设置错误码
if (ec) {                   // 如果设置了错误码(因为发生了错误)
    std::cout << "ERROR: " << ec.message() << "\n";
}

之后,我们还可以检查特定的错误码:

if (ec == std::errc::read_only_file_system) {   // 如果设置了特定的错误码
    std::cout << "ERROR: " << p << " is read-only\n";
}

注意这种情况下,我们仍然必须检查create_directory()的返回值

std::error_code ec;
if (!create_directory(p, ec)) { // 发生错误时设置错误码
    // 发生任何错误时
    std::cout << "can't create directory " << p << "\n";
    std::cout << "error: " << ec.message() << "\n";
}

然而,并不是所有的文件系统操作都提供这种能力(因为它们在正常情况下会返回一些值)。

类型error_code由C++11引入,它包含了一系列可移植的错误条件,例如 std::errc::read_only_filesystem。在POSIX兼容的系统上这些被映射为errno的值。

20.2.7 文件类型

不同的操作系统支持不同的文件类型。标准文件系统库中也考虑到了这一点,它定义了一个 枚举类型file_type,标准中定义了如下的值:

namespace std::filesystem {
    enum class file_type {
        regular, directory, symlink,
        block, character, fifo, socket,
        ...
        none, not_found, unknown,
    };
}

file_type的值列出了这些值的含义。

含义
regular 普通文件
directory 目录文件
symlink 符号链接文件
character 字符特殊文件
block 块特殊文件
fifo FIFO或者管道文件
socket 套接字文件
... 附加的实现定义的文件类型
none 文件的类型未知
unknown 文件存在但推断不出类型
not_found 虚拟的表示文件不存在的类型

操作系统平台可能会提供附加的文件类型值。然而,使用它们是不可移植的。 例如,Windows就提供了文件类型值junction,它被用于NTFS文件系统中的 NTFS junctions (也被称为软链接)。 它们被用作链接来访问同一台电脑上不同的子卷(盘)。

除了普通文件和目录之外,最常见的类型是符号链接,它是一种指向另一个位置的文件。 指向的位置可能有一个文件也可能没有。 注意有些操作系统和/或文件系统(例如FAT文件系统)完全不支持符号链接。 有些操作系统只支持普通文件的符号链接。 注意在Windows上需要特殊的权限才能创建符号链接,可以用mklink命令创建。

字符特殊文件、块特殊文件、FIFO、套接字都来自于UNIX文件系统。 目前,Visual C++并没有使用这四种类型中的任何一个。

如你所见,有一些特殊的值来表示文件不存在或者类型未知或者无法探测出类型。

在这一章的剩余部分我将使用两种广义的类型来代表相应的若干文件类型:

  • 其他文件 :除了普通文件、目录、符号链接之外的所有类型的文件。 库函数is_other()和这个术语相匹配。
  • 特殊文件 :下列类型的文件:字符特殊文件、块特殊文件、FIFO、套接字。

另外, 特殊文件 类型加上实现定义的文件类型就构成了 其他文件 类型。

20.3 路径操作

有很多处理文件系统的操作。这些操作涉及的关键类型是std::filesystem::path, 它表示一个可能存在也可能不存在的文件的绝对或相对的路径。

你可以创建路径、检查路径、修改路径、比较路径。 因为这些操作一般都不会访问实际的文件系统(例如不会检查文件是否存在,也不会解析符号链接), 所以它们的开销很小。因此,它们通常被定义为成员函数(如果这些操作既不是构造函数也不是运算符的话)。

20.3.1 创建路径

表创建路径列出了创建新的路径对象的方法。

调用 效果
path{charseq} 用一个字符序列初始化路径
path{beg, end} 用一个范围初始化路径
u8path(u8string) 用一个UTF-8字符串初始化路径
current_path() 返回当前工作目录的路径
temp_directory_path() 返回临时文件的路径

第一个构造函数以字符序列为参数,这里的字符序列代表一系列有效的方式:

  • 一个string
  • 一个string_view
  • 一个以空字符结尾的字符数组
  • 一个以空字符结尾的字符输入迭代器(指针)

注意current_path()temp_directory_path()都是开销较大的操作, 因为它们依赖于系统调用。如果给current_path()传递一个参数,它也可以用来 修改当前工作目录。

通过u8path()你可以使用UTF-8字符串创建可移植的路径。例如:

// 将路径p初始化为"Köln"(Cologne的德语名):
std::filesystem::path p{std::filesystem::u8path(u8"K\u00F6ln")};
...

// 用UTF-8字符串创建目录:
std::string utf8String = readUTF8String(...);
create_directory(std::filesystem::u8path(utf8String));

20.3.2 检查路径

表检查路径列出了检查路径p时可以调用的函数。 注意这些操作都不会访问底层的操作系统,因此都是path类的成员函数。

调用 效果
p.empty() 返回路径是否为空
p.is_absolute() 返回路径是否是绝对的
p.is_relative() 返回路径是否是相对的
p.has_filename() 返回路径是否既不是目录也不是根名称
p.has_stem() has_filename()一样
p.has_extension() 返回路径是否有扩展名
p.has_root_name() 返回路径是否包含根名称
p.has_root_directory() 返回路径是否包含根目录
p.has_root_path() 返回路径是否包含根名称或根目录
p.has_parent_path() 返回路径是否包含父路径
p.has_relative_path() 返回路径是否不止包含根元素
p.filename() 返回文件名(或者空路径)
p.stem() 返回没有扩展名的文件名(或者空路径)
p.extension() 返回扩展名(或者空路径)
p.root_name() 返回根名称(或者空路径)
p.root_directory() 返回根目录(或者空路径)
p.root_path() 返回根元素(或者空路径)
p.parent_path() 返回父路径(或者空路径)
p.relative_path() 返回不带根元素的路径(或者空路径)
p.begin() 返回路径元素的起点
p.end() 返回路径元素的终点

每一个路径要么是绝对的要么是相对的。如果没有根目录那么路径就是相对的 (相对路径也可能包含根名称;例如,C:hello.txt就是Windows下的一个相对路径)。

has_...()函数等价于检查相应的没有has_前缀的函数的返回值是否为空路径。

注意下面几点:

  • 如果路径含有根元素或者目录分隔符那么就包含父路径。如果路径只由根元素组成 (也就是说相对路径为空),parent_path()的返回值就是整个路径。 也就是说,路径"/"的父路径还是"/"。只有纯文件名的路径例如 "hello.txt"的父路径是空。

  • 如果一个路径包含文件名那么一定包含stem(文件名中不带扩展名的部分)。

  • 空路径是相对路径(除了empty()is_relative() 之外的操作都返回false或者空路径)。

这些操作的结果可能会依赖于操作系统。例如,路径 C:/hello.txt

  • 在Unix系统上

  • 是相对路径

  • 没有根元素(既没有根名称也没有根目录),因为C:只是一个文件名

  • 有父路径C:

  • 有相对路径C:/hello.txt

  • 在Windows系统上

  • 是绝对的

  • 有根名称C:和根目录/

  • 沒有父路径

  • 有相对路径hello.txt

遍历路径

你可以遍历一个路径,这将会返回路径的所有元素:根名称(如果有的话)、根目录(如果有的话)、 所有的文件名。如果路径以目录分隔符结尾,最后的元素将是空文件名。

路径迭代器是双向迭代器,所以你可以递减它。迭代器的值的类型是path。 然而,两个在同一个路径上迭代的迭代器可能 指向同一个path对象, 即使它们迭代到了相同的路径元素。

例如,考虑:

void printPath(const std::filesystem::path& p)
{
    std::cout << "path elements of \"" << p.string() << "\":\n";
    for (std::filesystem::path elem : p) {
        std::cout << "  \"" << elem.string() << '"';
    }
    std::cout << '\n';
}

和如下代码效果相同:

void printPath(const std::filesystem::path& p)
{
    std::cout << "path elements of \"" << p.string() << "\":\n";
    for (auto pos = p.begin(); pos != p.end(); ++pos) {
        std::filesystem::path elem = *pos;
        std::cout << "  \"" << elem.string() << '"';
    }
    std::cout << '\n';
}

如果像下面这样调用这个函数:

printPath("../sub/file.txt");
printPath("/usr/tmp/test/dir/");
printPath("C:\\usr\\tmp\\test\\dir\\");

在POSIX兼容系统上的输出将会是:

path elements of "../sub/file.txt":
".."  "sub"  "file.txt"
path elements of "/usr/tmp/test/dir/":
"/"  "usr"  "tmp"  "test"  "dir"  ""
path elements of "C:\\usr\\tmp\\test\\dir\\":
"C:\\usr\\tmp\\test\\dir\\"

注意最后一个路径只是一个文件名,因为在POSIX兼容系统上C:不是有效的根名称, 反斜杠也不是有效的目录分隔符。

在Windows上的输出将是:

path elements of "../sub/file.txt":
".."  "sub"  "file.txt"
path elements of "/usr/tmp/test/dir/":
"/"  "usr"  "tmp"  "test"  "dir"  ""
path elements of "C:\usr\tmp\test\dir\":
"C:"  "\"  "usr"  "tmp"  "test"  "dir"  ""

为了检查路径p是否以目录分隔符结尾,你可以这么写:

if (!p.empty() && (--p.end())->empty()) {
    std::cout << p << " has a trailing separator\n";
}

20.3.3 路径I/O和转换

表路径I/O和转换列出了路径的读写操作和转换操作。 这些函数也不会访问实际的文件系统。如果必须要处理符号链接, 你可能需要使用依赖文件系统的路径转换。

调用 效果
strm << p 用双引号括起来输出路径
strm >> p 读取用双引号括起来的路径
p.string() std::string返回路径
p.wstring() std::wstring返回路径
p.u8string() 以类型为std::u8string的UTF-8字符串返回路径
p.u16string() 以类型为std::u16string的UTF-16字符串返回路径
p.u32string() 以类型为std::u32string的UTF-32字符串返回路径
p.string<...>() std::basic_string<...>返回路径
p.lexically_normal() 返回正规化的路径
p.lexically_relative(p2) 返回从p2p的相对路径(如果没有则返回空路径)
p.lexically_proximate(p2) 返回从p2p的路径(如果没有则返回p

lexically_...()函数会返回一个新的路径,而其他的转换函数将返回相应的字符串类型。 所有这些函数都不会修改调用者的路径。

例如,下面的代码:

std::filesystem::path p{"/dir/./sub//sub1/../sub2"};
std::cout <<  "path:               " << p << '\n';
std::cout <<  "string():           " << p.string() << '\n';
std::wcout << "wstring():          " << p.wstring() << '\n';
std::cout <<  "lexically_normal(): " << p.lexically_normal() << '\n';

前三行的输出是相同的:

path:               "/dir/./sub//sub1/../sub2"
string():           /dir/./sub//sub1/../sub2
wstring():          /dir/./sub//sub1/../sub2

但最后一行的输出就依赖于目录分隔符了。在POSIX兼容系统上输出是:

lexically_normal(): "/dir/sub/sub2"

而在Windows上输出是:

lexically_normal(): "\\dir\\sub\\sub2"

路径I/O

首先,注意I/O运算符以双引号括起来的字符串方式读写路径。 你可以把它们转换为字符串来避免双引号:

std::filesystem::path file{"test.txt"};
std::cout << file << '\n';          // 输出:"test.txt"
std::cout << file.string() << '\n'; // 输出:test.txt

在Windows上,情况可能会更糟糕。下面的代码:

std::filesystem::path tmp{"C:\\Windows\\Temp"};
std::cout << tmp << '\n';
std::cout << tmp.string() << '\n';
std::cout << '"' << tmp.string() << "\"\n";

将会有如下输出:

"C:\\Windows\\Temp"
C:\Windows\Temp
"C:\Windows\Temp"

注意读取路径时既支持带双引号的字符串也支持不带双引号的字符串。 因此,所有的输出形式都能使用输入运算符再读取回来:

std::filesystem::path tmp;
std::cin >> tmp;    // 读取有双引号和无双引号的路径

正规化

当你处理可移植代码时正规化可能会导致更多令人惊奇的结果。例如:

std::filesystem::path p2{"//host\\dir/sub\\/./\\"};
// 译者注:此处原文是
// std::filesystem::path p2{"//dir\\subdir/subsubdir\\/./\\"};
// 应是作者笔误

std::cout << "p2:                 " << p2 << '\n';
std::cout << "lexically_normal(): " << p2.lexically_normal() << '\n';

在Windows系统上可能会有如下输出:

p2:                 "//host\\dir/sub\\/./\\"
lexically_normal(): "\\\\host\\dir\\sub\\"

然而,在POSIX兼容系统上,输出将是:

p2:                 "//host\\dir/sub\\/./\\"
lexically_normal(): "/host\\dir/sub\\/\\"

原因是对于POSIX兼容系统来说反斜杠既不是路径分隔符也不是有效的根名称, 这意味着我们得到了一个有三个文件名的绝对路径,三个文件名分别是host\dirsub\ \ 。 在POSIX兼容系统上,没有办法把反斜杠作为目录分隔符处理 (generic_string()make_preferred()也没有用)。 因此,对于可移植的代码,当处理路径时你应该总是使用通用路径格式。

但是,当遍历当前目录时使用lexically_normal() 移除开头的点是个好方法。

相对路径

lexically_relative()lexically_proximate()都可以被用来计算 两个路径间的相对路径。不同之处在于如果没有相对路径时的行为, 只有当一个是相对路径一个是绝对路径或者两个路径的根名称不同时才会发生这种情况。 这种情况下:

  • 对于p.lexically_relative(p2),如果没有从p2p 的相对路径,将会返回空路径。
  • 对于p.lexically_proximate(p2),如果没有从p2p 的相对路径,将会返回p

因为这两个操作都是词法操作,所以不会考虑实际的文件系统(可能会有符号链接)和current_path()。 如果两个路径相同,相对路径将是"."。例如:

fs::path{"/a/d"}.lexically_relative("/a/b/c");      // "../../d"
fs::path{"/a/b/c"}.lexically_relative("/a/d");      // "../b/c"
fs::path{"/a/b"}.lexically_relative("/a/b");        // "."
fs::path{"/a/b"}.lexically_relative("/a/b/");       // "."
fs::path{"/a/b"}.lexically_relative("/a/b\\");      // "."
fs::path{"/a/b"}.lexically_relative("/a/d/../c");   // "../b"
fs::path{"a/d/../b"}.lexically_relative("a/c");     // "../d/../b"
fs::path{"a//d/..//b"}.lexically_relative("a/c");   // "../d/../b"

在Windows平台上,则是:

fs::path{"C:/a/b"}.lexically_relative("c:/c/d");    // ""
fs::path{"C:/a/b"}.lexically_relative("D:/c/d");    // ""
fs::path{"C:/a/b"}.lexically_proximate("D:/c/d");   // "C:/a/b"

转换为字符串

通过u8string()你可以将路径用作UTF-8字符串, 这是当前存储数据的通用格式。例如:

// 把路径存储为UTF-8字符串:
std::vector<std::string> utf8paths; // 自从C++20起将改为std::u8string
for (const auto& entry : fs::directory_iterator(p)) {
    utf8paths.push_back(entry.path().u8string());
}

注意自从C++20起u8string()的返回值可能会从std::string改为 std::u8string()(新的UTF-8字符串类型和存储UTF-8字符 的char8_t类型的提案见https://wg21.link/p0482)。

成员模板string<>()可以用来转换成特殊的字符串类型,例如一个大小写无关的字符串类型:

struct ignoreCaseTraits : public std::char_traits<char> {
    // 大小写不敏感的比较两个字符:
    static bool eq(const char& c1, const char& c2) {
        return std::toupper(c1) == std::toupper(c2);
    }
    static bool lt(const char& c1, const char& c2) {
        return std::toupper(c1) < std::toupper(c2);
    }
    // 比较s1和s2的至多前n个字符:
    static int compare(const char* s1, const char* s2, std::size_t n);
    // 在s中搜索字符c:
    static const char* find(const char* s, std::size_t n, const char& c);
};

// 定义一个这种类型的字符串:
using icstring = std::basic_string<char, ignoreCaseTraits>;

std::filesystem::path p{"/dir\\subdir/subsubdir\\/./\\"};
icstring s2 = p.string<char, ignoreCaseTraits>();

注意你 应该使用函数c_str(),因为它会转换为 本地 字符串格式, 可能是wchar_t,因此你需要使用std::wcout代替std::cout 来输出到输出流。

20.3.4 本地和通用格式的转换

表通用路径格式和 实际平台特定实现的格式之间转换的方法。

调用 效果
p.generic_string() 返回std::string类型的通用路径
p.generic_wstring() 返回std::wstring类型的通用路径
p.generic_u8string() 返回std::u8string类型的通用路径
p.generic_u16string() 返回std::u16string类型的通用路径
p.generic_u32string() 返回std::u32string类型的通用路径
p.generic_string<...>() 返回std::basic_string<...>()类型的通用路径
p.native() 返回path::string_type类型的本地路径格式
到本地路径的转换 到本地字符串类型的隐式转换
p.c_str() 返回本地字符串格式的字符序列形式的路径
p.make_preferred() p中的目录分隔符替换为本地格式的分隔符并返回修改后的p

这些函数在POSIX兼容系统上没有效果,因为这些系统的本地格式和通用格式没有区别。 在其他平台上调用这些函数可能会有效果:

  • generic...()函数返回转换为通用格式之后的相应类型的字符串。
  • native()返回用本地字符串编码的路径,其类型为std::filesystem::path::string_type。 这个类型在Windows下是std::wstring,这意味着你需要使用std::wcout 代替std::cout来输出。新的重载允许我们向文件流传递本地字符串。
  • c_str()以空字符结尾的字符序列形式返回结果。注意使用这个函数是不可移植的, 因为使用std::cout打印字符序列在Windows上的输出不正确。你应该使用std::wcout
  • make_preferred()会使用本地的目录分隔符替换除了根名称之外的所有的目录分隔符。 注意这是唯一一个会修改调用者的函数。因此,严格来讲,这个函数应该属于下一节的修改路径的函数, 但因为它可以处理本地格式的转换,所以也在这里列出。

例如,在Windows上,下列代码:

std::filesystem::path p{"/dir\\subdir/subsubdir\\/./\\"};
std::cout <<  "p:                  " << p << '\n';
std::cout <<  "string():           " << p.string() << '\n';
std::wcout << "wstring():          " << p.wstring() << '\n';
std::cout <<  "lexically_normal(): " << p.lexically_normal() << '\n';
std::cout <<  "generic_string():   " << p.generic_string() << '\n';
std::wcout << "generic_wstring():  " << p.generic_wstring() << '\n';
// 因为这是在Windows下,相应的本地字符串类型是wstring:
std::wcout << "native():           " << p.native() << '\n'; // Windows!
std::wcout << "c_str():            " << p.c_str() << '\n';
std::cout <<  "make_preferred():   " << p.make_preferred() << '\n';
std::cout <<  "p:                  " << p << '\n';

将会有如下输出:

p:                  "/dir\\subdir/subsubdir\\/./\\"
string():           /dir\subdir/subsubdir\/./\
wstring():          /dir\subdir/subsubdir\/./\
lexically_normal(): "\\dir\\subdir\\subsubdir\\"
generic_string():   /dir/subdir/subsubdir//.//
generic_wstring():  /dir/subdir/subsubdir//.//
native():           /dir\subdir/subsubdir\/./\
c_str():            /dir\subdir/subsubdir\/./\
make_preferred():   "\\dir\\subdir\\subsubdir\\\\.\\\\"
p:                  "\\dir\\subdir\\subsubdir\\\\.\\\\"

再次注意:

  • 本地字符串格式是不可移植的。在Windows上是wstring,而在POSIX兼容系统上是string, 这意味着你要使用cout而不是wcout来打印native()的结果。
  • 只有make_preferred()会修改调用者。其他的调用都会保持p不变。

20.3.5 修改路径

表修改路径列出了可以直接修改路径的操作。

调用 效果
p = p2 赋予一个新路径
p = sv 赋予一个字符串(视图)作为新路径
p.assign(p2) 赋予一个新路径
p.assign(sv) 赋予一个字符串(视图)作为新路径
p.assign(beg, end) 赋予从begend的元素组成的路径
p1 / p2 返回把p2作为子路径附加到p1之后的结果
p /= sub sub作为子路径附加到路径p之后
p.append(sub) sub作为子路径附加到路径p之后
p.append(beg, end) 把从begend之间的元素作为子路径附加到路径p之后
p += str str里的字符添加到路径p之后
p.concat(str) str里的字符添加到路径p之后
p.concat(beg, end) 把从begend之间的元素附加到路径p之后
p.remove_filename() 移除路径末尾的文件名
p.replace_filename(repl) 替换末尾的文件名(如果有的话)
p.replace_extension() 移除末尾的文件的扩展名
p.replace_extension(repl) 替换末尾的文件的扩展名(如果有的话)
p.clear() 清空路径
p.swap(p2) 交换两个路径
swap(p1, p2) 交换两个路径
p.make_preferred() p中的目录分隔符替换为本地格式的分隔符并返回修改后的p

注意+=concat()简单的把字符添加到路径后,//=append()则是在路径后用目录分隔符添加一个子路径:

std::filesystem::path p{"myfile"};
p += ".git";        // p:myfile.git
p /= ".git";        // p:myfile.git/.git
p.concat("1");      // p:myfile.git/.git1
p.append("1");      // P:myfile.git/.git1/1
std::cout << p << '\n';
std::cout << p / p << '\n';

在POSIX兼容系统上输出将是:

"myfile.git/.git1/1"
"myfile.git/.git1/1/myfile.git/.git1/1"

在Windows系统上输出将是:

"myfile.git\\.git1\\1"
"myfile.git\\.git1\\1\\myfile.git\\.git1\\1"

注意如果添加一个绝对路径子路径意味着替换原本的路径。例如,如下操作之后:

namespace fs = std::filesystem;
auto p1 = fs::path("/usr") / "tmp";     // 路径是/usr/tmp或者/usr\tmp
auto p2 = fs::path("/usr/") / "tmp";    // 路径是/usr/tmp
auto p3 = fs::path("/usr") / "/tmp";    // 路径是/tmp
auto p4 = fs::path("/usr/") / "/tmp";   // 路径是/tmp

我们有了四个指向两个不同文件的路径:

  • p1p2相等,都指向文件/usr/tmp(注意在Windows上 它们相等,但p1将是/usr\tmp)。
  • p3p4相等,指向文件/tmp,因为附加的子路径是绝对路径。

对于根元素,是否赋予新的根元素的结果将不同。例如,在Windows上,结果将是:

auto p1 = fs::path("usr") / "C:/tmp";   // 路径是C:/tmp
auto p2 = fs::path("usr") / "C:";       // 路径是C:
auto p3 = fs::path("C:") / "":          // 路径是C:
auto p4 = fs::path("C:usr") / "/tmp";   // 路径是C:/tmp
auto p5 = fs::path("C:usr") / "C:tmp";  // 路径是C:usr\tmp
auto p6 = fs::path("C:usr") / "c:tmp";  // 路径是c:tmp
auto p7 = fs::path("C:usr") / "D:tmp";  // 路径是D:tmp

函数make_preferred()把一个路径内的目录分隔符替换成本地格式。 例如:

std::filesystem::path p{"//server/dir//subdir///file.txt"};
p.make_preferred();
std::cout << p << '\n';

在POSIX兼容系统上输出将是:

"//server/dir/subdir/file.txt"

在Windows系统上输出将是:

"\\\\server\\dir\\\\subdir\\\\\\file.txt"

注意开头的根名称没有被修改,因为它由两个斜杠或者反斜杠组成。 也注意这个函数在POSIX兼容系统上也不会把反斜杠转换成斜杠,因为反斜杠不被识别为目录分隔符。

replace_extension()可以替换、添加、或者删除扩展名:

  • 如果文件已经有扩展名了,它会进行替换。
  • 如果文件没有扩展名,会添加新的扩展名。
  • 如果你跳过了新的扩展名参数或者新扩展名参数为空,它会移除已有的扩展名。

替换时是否有前导的点没有影响。函数会确保文件名和扩展名之间有且只有一个点。例如:

fs::path{"file.txt"}.replace_extension("tmp")   // file.tmp
fs::path{"file.txt"}.replace_extension(".tmp")  // file.tmp
fs::path{"file.txt"}.replace_extension("")      // file
fs::path{"file.txt"}.replace_extension()        // file
fs::path{"dir"}.replace_extension("tmp")        // dir.tmp
fs::path{".git"}.replace_extension("tmp")       // .git.tmp

注意“纯扩展名”的文件名(例如.git)不会被当作扩展名。

20.3.6 比较路径

表比较路径列出了可以比较两个路径的操作。

调用 效果
p1 == p2 返回两个路径是否相等
p1 != p2 返回两个路径是否不相等
p1 < p2 返回一个路径是否小于另一个
p1 <= p2 返回一个路径是否小于等于另一个
p1 >= p2 返回一个路径是否大于等于另一个
p1 > p2 返回一个路径是否大于另一个
p.compare(p2) 返回p是小于、等于还是大于p2
p.compare(sv) 返回p是小于、等于还是大于字符串(视图)sv转换成的路径
equivalent(p1, p2) 访问实际文件系统的开销较大的比较操作

注意这些比较操作大部分都不会访问实际的文件系统,这意味着它们只是以词法的方式比较, 这样开销很小但可能会导致令人惊奇的结果:

  • 使用==!=compare()的结果是下面的路径都不相同:
tmp1/f
./tmp1/f
tmp1/./f
tmp1/tmp11/../f
  • 只有当分隔符以外的部分都相同时才会判定为相同。 因此,下面的路径是相等的(假设反斜杠也是有效的目录分隔符):
tmp1/f
tmp1//f
tmp1\f
tmp1/\/f

如果你使用了lexically_normal()那么上面的所有路径都相等 (假设反斜杠也是有效的目录分隔符)。例如:

std::filesystem::path p1{"tmp1/f"};
std::filesystem::path p2{"./tmp1/f"};

p1 == p2                                                // false
p1.compare(p2)                                          // 非0
p1.lexically_normal() == p2.lexically_normal()          // true
p1.lexically_normal().compare(p2.lexically_normal())    // 0

如果你想要访问实际的文件系统来正确处理包含符号链接的情况,你需要使用equivalent()。 然而,注意这个函数要求两个路径都代表已经存在的文件。 因此,一个通用的尽可能精确(但没有最佳的性能)的比较两个路径的方法是:

bool pathsAreEqual(const std::filesystem::path& p1,
                   const std::filesystem::path& p2)
{
    return exists(p1) && exists(p2) ? equivalent(p1, p2)
        : p1.lexically_normal() == p2.lexically_normal();
}

20.3.7 其他路径操作

表其他路径操作列出了剩余的路径操作。

调用 效果
p.hash_value() 返回一个路径的哈希值

注意只有相等的路径才保证有相同的哈希值,下面的路径返回不同的哈希值:

tmp1/f
./tmp1/f
tmp1/./f
tmp1/tmp11/../f

因此,在把它们放入哈希表之前,你可能需要先对路径进行正规化。

20.4 文件系统操作

这一节介绍开销更大的会访问实际文件系统的操作。

因为这些操作通常会访问文件系统(要确定文件是否存在、解析符号链接等等), 它们比纯路径操作的开销要大的多。因此,它们通常是独立函数。

20.4.1 文件属性

对于一个路径你可以查询很多文件属性。首先,表文件类型操作列出了 可以检查路径p是否指向特定的文件并查询它的类型(如果有的话)的操作。 注意这些操作都会访问实际的文件系统,也都是独立函数。

调用 效果
exists(p) 返回是否存在一个可访问到的文件
is_symlink(p) 返回是否文件p存在并且是符号链接
is_regular_file(p) 返回是否文件p存在并且是普通文件
is_directory(p) 返回是否文件p存在并且是目录
is_other(p) 返回是否文件p存在并且不是普通文件或目录或符号链接
is_block_file(p) 返回是否文件p存在并且是块特殊文件
is_character_file(p) 返回是否文件p存在并且是字符特殊文件
is_fifo(p) 返回是否文件p存在并且是FIFO或者管道文件
is_socket(p) 返回是否文件p存在并且是套接字

文件系统类型的函数和相应的file_type值相对应。 然而,注意这些函数(除了is_symlink()之外)都会解析符号链接。也就是说, 对于一个指向目录的符号链接,is_symlink()is_directory()都返回true

还要注意对于特殊文件(非普通文件、非目录、非符号链接)的检查, 根据其他文件类型的定义,is_other()会返回true

对于实现特定的文件类型没有明确的便捷函数,这意味着只有is_other()会返回 true(如果是一个指向这种文件的符号链接,那么is_symlink()也会返回true)。 你可以使用文件状态API来检测这些特定的类型。

如果不想解析符号链接,可以像下面将要讨论的exists()一样使用symlink_status(), 并对返回的file_status调用这些函数。

检查文件是否存在

exists()检查是否有一个可以打开的文件。像之前讨论的一样,它会解析符号链接。 因此,对于指向不存在文件的符号链接它会返回false

因此,下面的代码不能像预期中工作:

// 如果不存在文件p,创建一个符号链接p指向file:
if (!exists(p)) {   // OOPS:检查文件p指向的目标是否不存在
    std::filesystem::create_symlink(file, p);
}

如果p已经存在并且是一个指向不存在文件的符号链接, create_symlink()将会尝试在p位置处创建新的符号链接, 这会导致抛出一个相应的异常。

因为多用户/多进程操作系统中文件系统的状况可能会在任意时刻改变,最佳的方法就是尝试进行操作 并在失败时处理错误。因此,我们可以直接进行操作并处理相应的异常或者 传递额外的参数来处理错误码。

然而,有时你必须检测文件是否存在(在进行文件系统操作之前)。 例如,如果你想在指定位置创建一个文件并且该位置处已经有了一个符号链接, 那么新创建的文件(可能)会在一个意料之外的地方创建或者覆盖。 这种情况下,你应该像下面这样检查文件是否存在:

if (!exists(symlink_status(p))) {   // OK:检查p是否还不存在(作为符号链接)
    ...
}

这里,我们使用了symlink_status(), 它会在 解析符号链接的情况下返回文件状态,以检查位置p处是否有任何文件存在。

其他文件属性

表文件属性操作列出了一些检查额外文件属性的独立函数。

调用 效果
is_empty() 返回文件是否为空
file_size() 返回文件大小
hard_link_count(p) 返回硬链接数量
last_write_time(p) 返回最后一次修改文件的时间

注意路径是否为空和路径指向的文件是否为空是不同的:

p.empty()       // 如果路径p为空则返回true(开销很小)
is_empty(p)     // 如果路径p处的文件为空返回true(文件系统操作)

如果文件存在并且是普通文件,那么file_size(p)以字节为单位返回文件p的大小 (在POSIX系统上,这个值和stat()返回的st_size成员的值相同)。 对于所有其他文件,结果是实现特定的,因此不可移植。

hard_link_count(p)返回文件系统中某一个文件存在的次数。 通常情况下,这个数字是1,但是在某些文件系统上同一个文件可以出现在不同的位置(也就是有多个不同的路径)。 这和软链接指向其他文件不同,这里的路径可以直接访问文件。只有当最后一个硬链接被删除时文件才会被删除。

处理最后修改的时间

last_write_time(p)返回文件最后一次修改或者写入的时间。 返回的类型是标准chrono库里的时间点类型time_point

namespace std::filesystem {
    using file_time_type = chrono::time_point<travialClock>;
}

时钟类型 trivialClock 是一个实现特定的时钟类型,它能反应时钟的精度和范围。 例如,你可以这么使用它:

void printFileTime(const std::filesystem::path& p)
{
    auto filetime = last_write_time(p);
    auto diff = std::filesystem::file_time_type::clock::now() - filetime;
    std::cout << p << " is "
              << std::chrono::duration_cast<std::chrono::seconds>(diff).count()
              << " Seconds old.\n";
}

输出可能是:

"fileattr.cpp" is 4 Seconds old.

这个例子中,你可以使用:

decltype(filetime)::clock::now()

来代替

std::filesystem::file_time_type::clock::now()

注意文件系统时间点使用的时钟不保证是标准的system_clock。 因此,现在还没有把文件系统时间点转换为time_t然后在字符串或者输出中用作绝对时间的标准化支持。

然而还是有一些解决方法的,下面的代码“粗略地”把任何时钟的时间点转换为time_t对象:

template<typename TimePoint>
std::time_t toTimeT(TimePoint tp)
{
    using system_clock = std::chrono::system_clock;
    return system_clock::to_time_t(system_clock::now() + (tp - decltype(tp)::clock::now()));
}

技巧是计算出文件系统时间点和现在的差值,类型是duration,然后加上现在的系统时钟的时间 就可以得到用系统时钟表示的文件系统时间点。这个函数不是很精确,因为不同的时钟可能有不同的精度, 而且两次now()调用之间可能时间差。然而,一般来说,这个函数工作得很好。

例如,对一个路径p我们可以调用:

auto ftime = last_write_time(p);
std::time_t t = toTimeT(ftime);
// 转换为日历时间(跳过末尾的换行符):
std::string ts = ctime(&t);
ts.resize(ts.size()-1);
std::cout << "last access of " << p << ": " << ts << '\n';

输出可能是:

last access of "fileattr.exe": Sun Jun 24 10:41:12 2018

为了把时间格式化为我们想要的格式,我们可以这样调用:

std::time_t t = toTimeT(ftime);
char mbstr[100];
if (std::strftime(mbstr, sizeof(mbstr), "last access: %B %d, %Y at %H:%M\n",
                  std::localtime(&t))) {
    std::cout << mbstr;
}

输出可能是:

last access: June 24, 2018 at 10:41

把任意文件系统时间点转换为字符串的一个有用的辅助函数可以是:

#include <string>
#include <chrono>
#include <filesystem>

std::string asString(const std::filesystem::file_time_type& ft)
{
    using system_clock = std::chrono::system_clock;
    auto t = system_clock::to_time_t(system_clock::now()
                                     + (ft - std::filesystem::file_time_type::clock::now()));
    // 转换为日历时间(跳过末尾的换行符):
    std::string ts = ctime(&t);
    ts.resize(ts.size()-1);
    return ts;
}

注意ctime()strftime()是非线程安全的,不能在并发环境中使用。

参见其他文件属性小节查看相应的修改最后访问时间的API。

20.4.2 文件状态

为了避免文件系统访问,有一个特殊的类型file_status可以被用来存储并修改 被缓存的文件类型和权限。发生以下情况时可以设置状态:

  • 当使用表文件状态的操作中的方法访问某个指定路径的文件状态时
  • 当遍历目录时
调用 效果
status(p) 返回文件pfile_status(解析符号链接)
symlink_status(p) 返回文件pfile_status(不解析符号链接)

不同之处在于路径p是否解析符号链接,status()将会解析符号链接 并返回指向的文件的属性(文件状态也可能是没有文件),而symlink_status(p) 将会返回符号链接自身的状态。

file_status的操作列出了file_status类型的对象fs的所有操作。

调用 效果
exists(fs) 返回是否有文件存在
is_regular_file(fs) 返回是否有文件存在并且是普通文件
is_directory(fs) 返回是否有文件存在并且是目录
is_symlink(fs) 返回是否有文件存在并且是符号链接
is_other(fs) 返回是否有文件存在并且不是普通文件或目录或符号链接
is_character_file(fs) 返回是否有文件存在并且是字符特殊文件
is_block_file(fs) 返回是否有文件存在并且是块特殊文件
is_fifo(fs) 返回是否有文件存在并且是FIFO或者管道文件
is_socket(fs) 返回是否有文件存在并且是套接字
fs.type() 返回文件的file_type
fs.permissions() 返回文件的权限

使用状态操作的一个好处是可以节省同一个文件的多次操作系统调用。例如, 原本的如下代码:

if (!is_directory(path)) {
    if (is_character_file(path) || is_block_file(path)) {
        ...
    }
    ...
}

可以用如下方式更高效地实现:

auto pathStatus{status(path)};
if (!is_directory(pathStatus)) {
    if (is_character_file(pathStatus) || is_block_file(pathStatus)) {
        ...
    }
    ...
}

另一个关键的好处是通过使用symlink_status(),你可以在 不解析 任何符号链接的 情况下检查文件状态。这很有用,例如当你想检测指定路径处是否有文件存在时。

因为文件状态不使用操作系统,所以没有提供相应的返回错误码的版本。

以路径作为参数的exists()is_...()函数是 获取并检查文件状态的type()的缩写。例如,

is_regular_file(mypath)

是如下代码的缩写:

is_regular_file(status(mypath))

上面的代码又是下面代码的缩写:

status(mypath).type() == file_type::regular

20.4.3 权限

处理权限的模型来自于UNIX/POSIX的世界。用若干位来表示文件所有者、同组其他用户、其他用户的 读、写、执行/搜索的权限。另外,还有特殊的位表示“运行时设置用户ID”、“运行时设置组ID” 和粘贴位(或者其他操作系统特定的含义)。

表权限位列出了位域枚举类型perms的值,该类型定义在 std::filesystem中,表示一个或多个权限位。

枚举 8进制值 POSIX 含义
none 0 没有权限集
owner_read 0400 S_IRUSR 所属用户有读权限
owner_write 0200 S_IWUSR 所属用户有写权限
owner_exec 0100 S_IXUSR 所属用户有执行/搜索权限
owner_all 0700 S_IRWXU 所属用户有所有权限
group_read 040 S_IRGRP 同组用户有读权限
group_write 020 S_IWGRP 同组用户有写权限
group_exec 010 S_IXGRP 同组用户有执行/搜索权限
group_all 070 S_IRWXG 同组用户有所有权限
others_read 04 S_IROTH 其他用户有读权限
others_write 02 S_IWOTH 其他用户有写权限
others_exec 01 S_IXOTH 其他用户有执行/搜索权限
others_all 07 S_IRWXO 其他用户有所有权限
all 0777 所有用户有所有权限
set_uid 04000 S_ISUID 运行时设置用户ID
set_gid 02000 S_ISGID 运行时设置组ID
sticky_bit 01000 S_ISVTX 操作系统特定
mask 07777 所有可能位的掩码
unknown 0xFFFF 未知权限

你可以查询当前的权限并检查返回的perms对象。为了组合标记,你需要使用位运算。例如:

// 如果可写:
if ((fileStatus.permissions() &
    (fs::perms::owner_write | fs::perms::group_write | fs::perms::others_write))
    != fs::perms::none) {
    ...
}

一个较短(但可读性较差)的初始化位掩码的方法是直接使用相应的8进制值 和更宽松的枚举初始化特性:

// 如果可写:
if ((fileStatus.permissions() & fs::perms{0222}) != fs::perms::none) {
    ...
}

注意你必须把&表达式放在括号里,因为它的优先级低于比较运算符。 还要注意不能跳过比较,因为没有从位域枚举类型到bool的隐式类型转换。

作为另一个例子,为了像UNIX命令ls -l一样把权限位转换为字符串,你可以使用 如下的辅助函数:

#include <string>
#include <chrono>
#include <filesystem>

std::string asString(const std::filesystem::perms& pm) {
    using perms = std::filesystem::perms;
    std::string s;
    s.resize(9);
    s[0] = (pm & perms::owner_read)   != perms::none ? 'r' : '-';
    s[1] = (pm & perms::owner_write)  != perms::none ? 'w' : '-';
    s[2] = (pm & perms::owner_exec)   != perms::none ? 'x' : '-';
    s[3] = (pm & perms::group_read)   != perms::none ? 'r' : '-';
    s[4] = (pm & perms::group_write)  != perms::none ? 'w' : '-';
    s[5] = (pm & perms::group_exec)   != perms::none ? 'x' : '-';
    s[6] = (pm & perms::others_read)  != perms::none ? 'r' : '-';
    s[7] = (pm & perms::others_write) != perms::none ? 'w' : '-';
    s[8] = (pm & perms::others_exec)  != perms::none ? 'x' : '-';
    return s;
}

这允许你使用标准输出打印出文件的权限:

std::cout << "permissions: " << asString(stasus(mypath).permissions()) << '\n';

对于一个所有者拥有所有权限、其他用户拥有读和执行权限的文件,输出可能是:

permissions: rwxr-xr-x

然而,注意Windows的ACL(访问控制列表)并不是完全按照这套方案来划分权限的。 因此,当使用Visual C++时,可写的文件 总是 同时有读、写、执行位被设置 (即使它 不是 可执行文件),带有只写标志的文件通常有读和执行位设置。 这也影响了ACL

20.4.4 修改文件系统

你可以通过创建、删除或者修改已有文件来修改文件系统。

创建和删除文件

表创建和删除文件列出了通过路径p创建和删除文件的操作。

调用 效果
create_directory(p) 创建目录
create_directory(p, attrPath) 创建属性为attrPath的目录
create_directories(p) 创建目录和该路径中所有不存在的目录
create_hard_link(to, new) 为已存在文件to创建另一个文件系统项new
create_symlink(to, new) 创建指向to的符号链接new
create_directory_symlink(to, new) 创建指向目录to的符号链接new
copy(from, to) 拷贝任意类型的文件
copy(from, to, options) 以选项options拷贝任意类型的文件
copy_file(from, to) 拷贝文件(不能是目录或者符号链接)
copy_file(from, to, options) 以选项options拷贝文件
copy_symlink(from, to) 拷贝符号链接(to也指向from指向的文件)
remove(p) 删除一个文件或者空目录
remove_all(p) 删除路径p并递归删除所有子文件(如果有的话)

没有创建普通文件的函数。这可以通过标准I/O流库做到。例如,下面的语句会创建一个新的空文件 (如果不存在的话):

std::ofstream{"log.txt"};

创建一个或多个目录的函数返回指定目录是否被创建。 因此,如果指定目录已经存在的话并不会返回错误。

copy...()函数对特殊文件类型无效。默认情况下它们会:

  • 如果已经存在文件时报错
  • 不递归操作
  • 解析符号链接

这些默认行为可以通过参数options覆盖,该参数的类型是位域枚举类型copy_options, 定义在命名空间std::filesystem中。 表拷贝选项列出了所有可能的值。

copy_options 效果
none 默认值(值为0)
skip_existing 跳过覆盖已有文件
overwrite_existing 覆盖已有文件
update_existing 如果新文件更新的话覆盖已有文件
recursive 递归拷贝子目录和内容
copy_symlinks 拷贝符号链接为符号链接
skip_symlinks 忽略符号链接
directories_only 只拷贝目录
create_hard_links 创建新的硬链接而不是拷贝文件
create_symlinks 创建符号链接而不是拷贝文件(源路径必须是绝对路径,除非目标路径在当前目录下)

当创建目录的符号链接时,使用create_directory_symlink()create_symlink()更好,因为有些操作系统需要显式指明目标是一个目录。 注意第一个参数是相对于要创建的符号链接所在的目录的相对路径。 因此,要创建一个指向sub/file.txt的符号链接sub/slink,你必须调用:

std::filesystem::create_symlink("file.txt", "sub/slink");

语句

std::filesystem::create_symlink("sub/file.txt", "sub/slink");

将会创建一个指向sub/sub/file.txt的符号链接sub/slink

删除文件的函数有以下行为:

  • remove()删除一个文件或者空目录。 如果没有要删除的文件/目录或者不能被删除时返回false而不会抛出异常。
  • remove_all()递归删除一个文件或者目录。它返回一个uintmax_t 类型的值表示删除的文件数量。如果没有文件时返回0,如果出错时返回uintmax_t(-1) 而不会抛出异常。

在这两种情况下,都会删除符号链接本身而不是它们指向的文件。

注意当传递字符串字面量作为参数时你应该总是正确地使用完全限定的remove(), 否则可能会调用C函数remove()

修改已存在的文件

表修改文件列出了修改已存在文件的操作。

调用 效果
rename(old, new) 重命名并/或移动文件
last_write_time(p, newtime) 修改最后修改时间
permissions(p, prms) prms替换文件权限
permissions(p, prms, mode) 根据mode修改文件权限
resize_file(p, newSize) 修改普通文件的大小

rename()可以处理任何类型的文件(包括目录和符号链接)。对于符号链接,它会重命名符号链接本身, 而不是指向的文件。注意rename()需要完整的包含文件名的目标路径:

// 移动"tmp/sub/x"到"tmp/x":
std::filesystem::rename("tmp/sub/x", "tmp");    // ERROR
std::filesystem::rename("tmp/sub/x", "tmp/x");  // OK

last_write_time()使用在“处理最后修改的时间”小节介绍的时间点格式。例如:

// 创建文件p(更新最后修改时间):
last_write_time(p, std::filesystem::file_time_type::clock::now());

permissions()使用了在“权限”小节描述的API。可选的mode 参数是一个位域枚举类型,定义在命名空间std::filesystem中。一方面,它允许你在 replaceaddremove之间选择。另一方面,使用nofollow 可以让你修改符号链接本身的权限而不是它们指向的文件的权限。 例如:

// 移除同组用户的写权限和其他用户的所有权限:
permissions(mypath, std::filesystem::perms::group_write | std::filesystem::perms::others_all,
                    std::filesystem::perm_options::remove);

再次注意Windows支持的权限概念有些不同。它的可移植的修改权限

  • 所有用户可读、写、执行/搜索(rwxrwxrwx
  • 所有用户可读、执行/搜索(r-xr-xr-x

为了在两种模式之间可移植地进行切换,你必须同时启用或者禁用三个写权限 (只删除一个没有用):

// 可移植地启用/禁用写权限选项值:
auto allWrite = std::filesystem::perms::owner_write
                | std::filesystem::perms::group_write
                | std::filesystem::perms::others_write;
// 可移植地移除写权限:
permissions(file, allWrite, std::filesystem::perm_options::remove);

一个更短(但可读性较差)的方法是以如下方式初始化allWrite (使用更宽松的枚举初始化特性):

std::filesystem::perms allWrite{0222};

resize_file()可以用来缩减或者扩展普通文件的大小。例如:

// 清空文件:
resize_file(file, 0);

20.4.5 符号链接和依赖文件系统的路径转换

表文件系统路径转换列出了所有访问实际文件系统的处理路径的操作。 当你想处理符号链接时这些操作尤其重要。使用纯路径转换开销更小但不会 访问实际的文件系统。

调用 效果
read_symlink(symlink) 返回符号链接指向的文件
absolute(p) 返回p的绝对路径(不解析符号链接)
canonical(p) 返回已存在的p的绝对路径(解析符号链接)
weakly_canonical(p) 返回p的绝对路径(解析符号链接)
relative(p) 返回从当前目录到p的相对(或空)路径
relative(p, base) 返回从basep的相对(或空)路径
proximate(p) 返回从当前目录到p的相对(或绝对)路径
proximate(p, base) 返回从basep的相对(或绝对)路径

注意根据文件是否必须存在、路径是否正规化、是否解析符号链接这些函数有不同的行为。 表文件系统路径转换属性列出了这些函数的需求和行为。

调用 必须存在 正规化 解析符号链接
read_symlink() Yes Yes Once
absolute() No Yes No
canonical() Yes Yes All
weakly_canonical() No Yes All
relative() No Yes All
proximate() No Yes All

下面的函数演示了处理符号链接时这些函数的使用方法和效果:

#include <filesystem>
#include <iostream>

void testSymLink(std::filesystem::path top)
{
    top = absolute(top);    // 转换为绝对路径,用于切换当前目录
    create_directory(top);  // 确保top存在
    current_path(top);      // 然后切换当前目录
    std::cout << std::filesystem::current_path() << '\n'; // 打印top的路径

    // 定义我们自己的子目录(还没有创建):
    std::filesystem::path px{top / "a/x"};
    std::filesystem::path py{top / "a/y"};
    std::filesystem::path ps{top / "a/s"};

    // 打印出一些相对路径(这些文件还不存在):
    std::cout << px.relative_path() << '\n';        // 从当前路径到px的相对路径
    std::cout << px.lexically_relative(py) << '\n'; // 从py到px的路径:"../x"
    std::cout << relative(px, py) << '\n';          // 从py到px的路径:"../x"
    std::cout << relative(px) << '\n';              // 从当前路径到px的路径:"a/x"

    std::cout << px.lexically_relative(ps) << '\n'; // 从ps到px的路径:"../x"
    std::cout << relative(px, ps) << '\n';          // 从ps到px的路径:"../x"

    // 现在创建子目录和符号链接
    create_directories(px);                         // 创建"top/a/x"
    create_directories(py);                         // 创建"top/a/y"
    if (!is_symlink(ps)) {
        create_directory_symlink(top, ps);          // 创建"top/a/s" -> "top"
    }
    std::cout << "ps: " << ps << " -> " << read_symlink(ps) << '\n';

    // 观察词法处理和文件系统处理的相对路径的不同:
    std::cout << px.lexically_relative(ps) << '\n'; // ps到px的路径:"../x"
    std::cout << relative(px, ps) << '\n';          // ps到px的路径:"a/x"
}

注意我们首先把一个可能是相对路径的路径转换成了绝对路径, 否则切换当前路径时当前路径可能会影响切换到的位置。 lexically_relative()都是 开销很小的成员函数,不会访问实际的文件系统。因此,它们会忽略符号链接。

独立函数relative()会访问实际的文件系统。当文件不存在时它的行为和lexically_relative()一样。 然而,在创建了符号链接ps(指向top)之后,它会解析符号链接并给出一个不同的结果。

在POSIX系统上,在当前路径"/tmp"以参数"top"调用上述函数将有如下输出:

"/tmp/top"
"tmp/top/a/x"
"../x"
"../x"
"a/x"
"../x"
"../x"
ps: "tmp/top/a/s" -> "/tmp/top"
"../x"
"a/x"

在Windows系统上,在当前路径"C:/temp"以参数"top"调用 上述函数会有如下输出:

"C:\\temp\\top"
"temp\\top\\a/x"
"..\\x"
"..\\x"
"a\\x"
"..\\x"
"..\\x"
ps: "C:\\temp\\top\\a/s" -> "C:\\temp\\top"
"..\\x"
"a\\x"

再次注意在Windows上你需要管理员权限才能创建符号链接。

20.4.6 其他文件系统操作

表其他操作列出了所有还未提到的其他文件系统操作。

调用 效果
equivalent(p1, p2) 返回是否p1p2指向同一个文件
space(p) 返回路径p的磁盘空间信息
current_path(p) 将当前工作目录设置为p

equivalent()函数在关于路径比较的小节介绍。

space()的返回值是如下的结构体:

namespace std::filesystem {
    struct space_info {
        uintmax_t capacity;
        uintmax_t free;
        uintmax_t available;
    };
}

因此,使用结构化绑定,你可以像下面这样打印出根目录可用的磁盘空间:

auto [cap, _, avail] = std::filesystem::space("/");
std::cout << std::fixed << std::precision(2)
          << avail/1.0e6 << " of " << cap/1.0e6 << " MB available\n\n";

输出可能是:

43019.82 of 150365.79 MB available

current_path()调用将整个程序(也会应用于所有线程) 的当前目录设置为参数指示的路径。通过下面的代码,你可以切换到另一个工作目录 并在离开作用域时恢复旧的工作目录:

// 保存当前路径:
auto currentDir{std::filesystem::current_path()};

try {
    // 临时切换当前路径:
    std::filesystem::current_path(subdir);
    ...     // 执行一些操作
}
catch (...) {
    // 发生异常时恢复当前路径:
    std::filesystem::current_path(currentDir);
    throw;  // 重新抛出
}
// 没有异常时恢复当前路径:
std::filesystem::current_path(currentDir);

20.5 遍历目录

文件系统库的一个关键作用就是遍历一个文件系统(子)树中的所有文件。

能做到这一点的最快捷的方法就是使用范围for循环。 你可以遍历一个目录中的所有文件:

for (const auto& e : std::filesystem::directory_iterator(dir)) {
    std::cout << e.path() << '\n';
}

或者递归遍历一个文件系统(子)树:

for (const auto& e : std::filesystem::recursive_directory_iterator(dir)) {
    std::cout << e.path() << '\n';
}

传入的参数dir可以是一个path也可以是其他可以隐式转换为路径的类型 (特指各种形式的字符串)。

注意e.path()返回包含遍历起点目录的文件名。因此,如果我们遍历"." 里的一个文件file.txt,文件名将是./file.txt或者.\file.txt

另外,路径被写入输出流时会带有双引号,所以输出时将变为"./file.txt" 或者".\\file.txt"。 因此,就像之前的最开始的例子,下面的循环可移植性更强:

for (const auto& e : std::filesystem::directory_iterator(dir)) {
    std::cout << e.path().lexically_normal().string() << '\n';
}

为了遍历当前目录,传递"."作为当前目录而不是""。 在Windows上传递一个空路径是可以的但不可移植。

目录迭代器表示一个范围

你可能会很惊讶你可以把一个迭代器传递给范围for循环,因为正常情况下应该传递一个范围。

技巧在于directory_iteratorrecursive_directory_iterator 都有提供全局的begin()end()函数重载:

  • begin()返回迭代器自身。
  • end()返回尾后迭代器,可以使用默认构造函数创建。

因此,你可以像下面这样遍历:

std::filesystem::directory_iterator di{p};
for (auto pos = begin(di); pos != end(di); ++pos) {
    std::cout << pos->path() << '\n';
}

或者像下面这样:

for (std::filesystem::directory_iterator pos{p};
     pos != std::filesystem::directory_iterator{};
     ++pos) {
    std::cout << pos->path() << '\n';
}

目录迭代器选项

当遍历目录时,你可以传递directory_options类型的值,这些值在 表目录迭代器选项中列出。 这个类型是一个位域的枚举类型,定义在命名空间 std::filesystem中。

directory_options 效果
none 默认情况(值为0)
follow_directory_symlink 解析符号链接(而不是跳过它们)
skip_permission_denied 当权限不足时跳过目录

默认行为是不解析符号链接并在没有权限访问(子)目录时抛出异常。 使用skip_permission_denied选项可以忽略那些没有权限访问的目录。

filesystem/createfiles.cpp 展示了follow_directory_symlink选项的一个应用。

20.5.1 目录项

目录迭代器的元素类型是std::filesystem::directory_entry。 因此,如果目录迭代器有效的话,使用operator*()将会返回这个类型。 这意味着范围for循环的正确类型如下所示:

for (const std::filesystem::directory_entry& e : std::filesystem::directory_iterator(p)) {
    std::cout << e.path() << '\n';
}

目录项包含一个path对象和一些附加的属性,例如硬链接数量、文件状态、文件大小、上次 修改时间、是否是符号链接、如果是的话它指向的实际路径等。

注意这个迭代器是输入迭代器。因为目录项随时都可能变化,所以重复遍历一个目录可能会得到不同的结果, 在并行算法中使用目录迭代器时必须考虑到这一点。

表目录项操作列出了目录项e的所有操作。 它们基本都是一些查询文件属性、获取文件状态、 检查权限、比较路径的操作。

调用 效果
e.path() 返回当前目录项的文件系统路径
e.exists() 返回文件是否存在
e.is_regular_file() 返回是否文件存在并且是普通文件
e.is_directory() 返回是否文件存在并且是目录
e.is_symlink() 返回是否文件存在并且是符号链接
e.is_other() 返回是否文件存在并且不是普通文件或目录或符号链接
e.is_block_file() 返回是否文件存在并且是块特殊文件
e.is_character_file() 返回是否文件存在并且是字符特殊文件
e.is_fifo() 返回是否文件存在并且是FIFO或者管道文件
e.is_socket() 返回是否文件存在并且是套接字
e.file_size() 返回文件大小
e.hard_link_count() 返回硬链接数量
e.last_write_time() 返回最后一次修改时间
e.status() 返回文件p的状态
e.symlink_status() 返回文件p的文件状态(解析符号链接)
e1 == e2 返回两个目录项的路径是否相等
e1 != e2 返回两个目录项的路径是否不相等
e1 < e2 返回是否一个目录项的路径小于另一个
e1 <= e2 返回是否一个目录项的路径小于等于另一个
e1 >= e2 返回是否一个目录项的路径大于等于另一个
e1 > e2 返回是否一个目录项的路径大于另一个
e.assign(p) p替换e的路径并更新目录项的所有属性
e.replace_filename(p) p替换e的路径中的文件名并更新目录项的所有属性
e.refresh() 更新该目录项所有缓存的属性

assign()replace_filename()会 调用相应的修改路径操作, 但不会修改底层文件系统中的文件。

目录项缓存

鼓励实现 缓存 额外的文件属性来避免使用目录项时额外的文件系统访问开销。 然而,实现并不是必须缓存数据,因为这样一些操作的开销会变大。

因为所有的值通常会被缓存,因此这些调用通常开销很小:

for (const auto& e : std::filesystem::directory_iterator{"."}) {
    auto t = e.last_write_time();   // 通常开销很小
    ...
}

不管是否有缓存,但在多用户或者多进程的操作系统中,所有这些迭代都可能返回不再有效的数据。 文件的大小和内容可能改变、文件可能被删除或者替换(因此,文件的类型也有可能改变)、 权限也可能被修改。

在这种情况下,你可以要求更新目录项持有的数据:

for (const auto& e : std::filesystem::directory_iterator{"."}) {
    ...             // 数据已经失效
    e.refresh();    // 刷新文件的缓存数据
    if (e.exists()) {
        auto t = e.last_write_time();
        ...
    }
}

或者,你可以总是查询当前的状态:

for (const auto& e : std::filesystem::directory_iterator{"."}) {
    ...             // 数据已经失效
    if (exists(e.path()) {
        auto t = last_write_time(e.path());
        ...
    }
}