为什么std::filesystem提供这么多非成员函数?

Why does std::filesystem provide so many non-member functions?

举个例子 file_size。要获取文件的大小,我们将使用

std::filesystem::path p = std::filesystem::current_path();
// ... usual "does this exist && is this a file" boilerplate
auto n = std::filesystem::file_size(p);

这没有错,如果它是普通的 C,但被告知 C++ 是一种 OO 语言 [我知道它是多范式,向我们的语言律师道歉:-)] 感觉如此... 命令(不寒而栗)对我来说,我已经开始期待对象式

auto n = p.file_size();

相反。其他函数也是如此,例如 resize_fileremove_file 甚至更多。

您知道 Boost 和 std::filesystem 选择这种命令式风格而不是对象式风格的任何理由吗?有什么好处? Boost mentions the rule(在最底部),但没有理由。

我在考虑固有的问题,例如 premove_file(p) 之后的状态,或者错误标志(带有附加参数的重载),但是这两种方法都没有解决这些问题。


你可以观察到迭代器的类似模式,现在我们可以(应该?)做 begin(it) 而不是 it.begin(),但我认为这里的基本原理是更符合使用非修改 next(it) 等。

文件系统库在 filesystem::path 类型和访问实际文件的操作之间有一个非常明确的区分,它代表一个抽象路径名(甚至不是一个存在的文件名)物理文件系统,即在磁盘上读取+写入数据。

你甚至指出了对此的解释:

The design rule is that purely lexical operations are supplied as class path member functions, while operations performed by the operating system are provided as free functions.

就是这个原因。

理论上可以在没有磁盘的系统上使用 filesystem::pathpath class 仅保存一串字符并允许操作该字符串,在字符集之间进行转换并使用一些规则来定义主机 OS 上文件名和路径名的结构。例如,它知道目录名称在 POSIX 系统上由 / 分隔,在 Windows 上由 \ 分隔。操作 path 中保存的字符串是 "lexical operation",因为它只执行字符串操作。

被称为"filesystem operations"的非成员函数是完全不同的。它们不只是使用抽象的 path 对象,它只是一串字符,它们执行访问文件系统的实际 I/O 操作(stat 系统调用,open, readdir 等)。这些操作采用 path 参数来命名要操作的文件或目录,然后它们访问真实的文件或目录。他们不只是在内存中操作字符串。

这些操作依赖于 OS 提供的 API 来访问文件,并且它们依赖于可能以与内存中字符串操作完全不同的方式失败的硬件。磁盘可能已满,或者可能在操作完成前被拔掉,或者可能有硬件故障。

这么看,file_size当然不是path的成员,因为跟路径本身没有关系。路径只是文件名的表示,而不是实际文件的表示。函数 file_size 查找具有给定名称的物理文件并尝试读取其大小。这不是文件 name 的 属性,它是文件系统上持久文件的 属性。与保存文件名的内存中的字符串完全分开的东西。

换句话说,我可以有一个 path 对象,其中包含完全无意义的内容,例如 filesystem::path p("hgkugkkgkuegakugnkunfkw"),这很好。我可以追加到那个路径,或者询问它是否有根目录等。但是如果这样的文件不存在,我就无法读取它的大小。我可以有一个确实存在的文件的路径,但我没有访问权限,比如 filesystem::path p("/root/secret_admin_files.txt"); ,这也很好,因为它只是一串字符。当我尝试使用文件系统操作函数访问该位置的某些内容时,我只会收到 "permission denied" 错误。

因为 path 成员函数从不接触文件系统,所以它们永远不会因权限或不存在的文件而失败。这是一个有用的保证。

You can observe a similar pattern with iterators, where nowadays we can (are supposed to?) do begin(it) instead of it.begin(), but here I think the rationale was to be more in line with the non-modifying next(it) and such.

不,这是因为它同样适用于数组(不能有成员函数)和 class 类型。如果你知道你正在处理的类似范围的东西是容器而不是数组那么你可以使用 x.begin() 但是如果你正在编写通用代码并且不知道它是容器还是数组然后 std::begin(x) 在这两种情况下都有效。

这两件事(文件系统设计和非成员范围访问函数)的原因并不是一些反 OO 偏好,它们是出于更明智、更实际的原因。基于它们中的任何一个都是糟糕的设计,因为它对一些喜欢 OO 的人感觉 更好,或者对不喜欢 OO 的人感觉更好。

此外,当一切都是成员函数时,有些事情是你不能做的:

struct ConvertibleToPath {
  operator const std::filesystem::path& () const;
  // ...
};

ConvertibleToPath c;
auto n = std::filesystem::file_size(c);  // works fine

但是如果 file_sizepath 的成员:

c.file_size();   // wouldn't work
static_cast<const std::filesystem::path&>(c).file_size(); // yay, feels object-ish!

几个原因(虽然有点推测,我没有密切关注标准化过程):

  1. 因为它是基于 boost::filesystem 设计的。现在,您可以问 "Why is boost::filesystem designed that way?",这将是一个公平的问题,但鉴于它确实如此,并且它已经看到了很多里程,它被接受到标准中,几乎没有变化。其他一些 Boost 构造也是如此(尽管有时会有一些变化,但主要是在幕后)。

  2. 设计 classes 时的一个共同原则是 "if a function doesn't need access to a class' protected/private members, and can instead use existing members - you don't make it a member as well." 虽然不是每个人都这么认为 - 但 boost::filesystem 的设计者似乎这样做了。

    std::string() 的上下文中查看对此的讨论(和论证),一个 "monolith" class 与无数的方法,作者是 C++ 名人 Hebert Sutter,在 Guru of the Week #84.

  3. 预计在 C++17 中我们可能已经有了统一调用语法(参见 Bjarne 的 Stroustrup 高度可读 proposal)。如果该标准已被接受,请调用

    p.file_size();
    

    相当于调用

    file_size(p);
    

    所以你可以选择任何你喜欢的。基本上.

已经发布了几个很好的答案,但它们没有触及问题的核心:在所有其他条件相同的情况下,如果您可以将某些东西实现为免费的非友元函数,您总是应该.

为什么?

因为,免费的非友元函数没有访问状态的特权。测试 classes 比测试函数要难得多,因为您必须说服自己,无论调用哪个成员函数,甚至是成员函数的组合,都会保持 class' 不变量。 member/friend 功能越多,您要做的工作就越多。

自由函数可以独立推理和测试。因为他们没有访问 class 状态的特权,所以他们不可能违反任何 class 不变量。

我不知道不变量和特权访问 path 允许的细节,但显然他们能够实现很多功能作为免费功能,他们做出了正确的选择并这样做了.

Scott Meyers brilliant article on this topic,给出 "algorithm" 是否使函数成为成员。

这是Herb Sutter bemoaning the massive interface of std::string。为什么?因为,string 的大部分接口都可以作为自由函数实现。有时使用起来可能有点笨拙,但它更容易测试、推理、改进封装和模块化、为以前没有的代码重用打开机会等。

除了其他人已经说过的。 人们对 "nonmember" 方法不满意的原因之一是需要在 API 前面键入 std::filesystem:: 或使用 using 指令。 但实际上您不必这样做,只需像这样跳过 API 调用的名称空间:

#include <iostream>
#include <filesystem>

int main()
{
    auto p = std::filesystem::path{"/bin/cat"};
    //notice file_size below has no namespace qualifiers
    std::cout << "Binary size for your /bin/cat is " << file_size(p);
}

工作得很好,因为由于 ADL,函数名称也会在其参数的命名空间中查找。

(现场样本https://wandbox.org/permlink/JrFz8FJG3OdgRwg9)