10

例如考虑 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提到了规则(在最底部),但没有理由。

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


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

4

4 回答 4

12

filesystem::pathFilesystem 库在表示抽象路径名(甚至不是存在的文件名)的类型和访问实际物理文件系统的操作(即在磁盘上读+写数据)之间有非常明确的分离.

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

设计规则是纯词法操作作为类路径成员函数提供,而操作系统执行的操作作为自由函数提供。

这就是原因。

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

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

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

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

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

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

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

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

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

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

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

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

但如果file_size是以下成员path

c.file_size();   // wouldn't work
static_cast<const std::filesystem::path&>(c).file_size(); // yay, feels object-ish!
于 2017-03-27T19:21:05.923 回答
12

已经发布了一些很好的答案,但它们并没有触及问题的核心:所有其他条件都相同,如果您可以将某些东西实现为免费的非朋友功能,那么您总是应该这样做。

为什么?

因为,免费的、非朋友的功能,没有特权访问状态。测试类比测试函数要困难得多,因为无论调用哪些成员函数,甚至是成员函数的组合,您都必须说服自己维护类的不变量。您拥有的会员/朋友功能越多,您要做的工作就越多。

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

我不知道哪些不变量和哪些特权访问path允许的详细信息,但显然他们能够将许多功能实现为自由函数,并且他们做出了正确的选择并且这样做了。

Scott Meyers 关于这个主题的精彩文章,给出了是否让函数成为成员的“算法”。

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

于 2017-03-27T19:38:36.843 回答
1

几个原因(虽然有点推测,但我并没有非常密切地遵循标准化过程):

  1. 因为它是基于boost::filesystem这样设计的。现在,您可以问“为什么要boost::filesystem这样设计?”,这将是一个公平的问题,但鉴于它确实如此,而且它已经以这种方式运行了很多里程,它被接受到标准中,几乎没有什么变化. 其他一些 Boost 结构也是如此(尽管有时会有一些变化,主要是在幕后)。

  2. 设计类时的一个共同原则是“如果一个函数不需要访问类的受保护/私有成员,而是可以使用现有成员 - 你也不要让它成为成员。” 虽然不是每个人都认为这一点 - 似乎是设计师boost::filesystem做的。

    请参阅 C++ 杰出人物 Hebert Sutter 在Guru of the Week #84的上下文中对此的讨论(和一个论点),这是std::string()一个具有无数方法的“单体”类。

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

    p.file_size();
    

    相当于打电话

    file_size(p);
    

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

于 2017-03-27T19:29:14.027 回答
0

除了其他人已经说过的。人们对“非成员”方法不满意的原因之一是需要在 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

于 2018-11-01T17:13:37.853 回答