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

dlw*_*dlw 10 c++ boost-filesystem c++17 std-filesystem

例如,考虑一下 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);
Run Code Online (Sandbox Code Playgroud)

这没有什么不对,如果它是普通的C,但已经被教导C++是一种OO语言[我知道这是多范式,向我们的语言律师道歉:-)]只是感觉如此...... 势在必行(对我来说不好意思,我已经开始期待对象了

auto n = p.file_size();
Run Code Online (Sandbox Code Playgroud)

代替.对于其他功能也是如此,例如resize_file,remove_file甚至可能更多.

你知道为什么Boost std::filesystem选择这种命令式而不是对象式的任何理由吗?有什么好处?提升提到了规则(在最底层),但没有理由.

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


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

Jon*_*ely 12

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

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

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

这就是原因.

理论上可以filesystem::path在没有磁盘的系统上使用.的path类只保持字符的字符串,并且允许操纵该字符串,字符集之间转换,并使用该定义对主机OS的文件名和路径名的结构中的一些规则.例如,它知道目录名/在POSIX系统上和\在Windows 上分开.操纵a中保存的字符串path是一个"词法操作",因为它只是执行字符串操作.

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

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

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

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

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

你可以用迭代器观察一个类似的模式,现在我们可以(应该?)开始(它)而不是it.begin(),但在这里我认为理由是更符合非修改下一个(它)等.

不,这是因为它对数组(不能有成员函数)和类类型同样有效.如果你知道你正在处理的类似范围的东西是一个容器而不是一个数组,那么你可以使用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
Run Code Online (Sandbox Code Playgroud)

但如果file_size是以下成员path:

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


Nir*_*man 11

已经发布了一些好的答案,但它们并没有解决问题的核心:所有其他条件相同,如果你可以实现一些免费的,非朋友的功能,你总是应该这样做.

为什么?

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

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

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

斯科特迈耶斯关于这个主题的精彩文章,给出了是否使一个功能成为成员的"算法".

这是Herb Sutter哀叹的巨大界面std::string.为什么?因为,很多string界面都可以作为自由函数实现.有时使用它可能有点笨拙,但它更容易测试,推理,改进封装和模块化,为以前没有的代码重用打开机会,等等.

  • 有没有人制作过草本版的std :: string,它真的会更容易使用吗? (4认同)