为什么C++标准文件流不能更严格地遵循RAII约定?

2022-09-05 00:00:00 iostream c++ c++-standard-library raii
为什么C++标准库流使用与对象生存期分离的open()/close()语义?从技术上讲,关闭销毁可能仍然会使类成为RAII类,但获取/释放独立性会在作用域中留下漏洞,其中句柄可以什么都不指向,但仍然需要运行时检查才能捕获。

为什么库设计者选择他们的方法,而不是只在引发失败的构造函数中打开?

void foo() {
  std::ofstream ofs;
  ofs << "Can't do this!
"; // XXX
  ofs.open("foo.txt");

  // Safe access requires explicit checking after open().
  if (ofs) {
    // Other calls still need checks but must be shielded by an initial one.
  }

  ofs.close();
  ofs << "Whoops!
"; // XXX
}

// This approach would seem better IMO:
void bar() {
  std_raii::ofstream ofs("foo.txt"); // throw on failure and catch wherever
  // do whatever, then close ofs on destruction ...
}

这个问题的一个更好的措辞可能是,为什么访问未打开的fstream值得拥有。在我看来,通过句柄生存期控制打开文件的持续时间并不是一种负担,而实际上是一种安全益处。


解决方案

虽然其他答案有效且有用,但我认为实际原因更简单。

IOSTREAMS设计比许多标准库都要老得多,而且早于异常的广泛使用。我怀疑,为了与现有代码兼容,使用异常是可选的,而不是无法打开文件时的默认设置。

另外,您的问题实际上只与文件流相关,其他类型的标准流没有open()close()成员函数,所以如果文件无法打开,它们的构造函数不会抛出:-)

对于文件,您可能希望检查close()调用是否成功,这样您就知道数据是否已写入磁盘,因此不在析构函数中执行此操作是一个很好的理由,因为当对象被销毁时,对其执行任何有用的操作都为时已晚,而且您几乎肯定不想从析构函数引发异常。因此,fstreambuf将在其析构函数中调用Close,但如果您愿意,也可以在销毁之前手动执行此操作。

无论如何,我不同意它不遵循RAII惯例...

为什么库设计者选择他们的方法,而不是只在引发失败的构造函数中打开?

N.B.RAII并不意味着除了资源获取构造函数之外不能拥有单独的open()成员,或者不能在销毁前清除资源,例如unique_ptrreset()成员。

此外,RAII并不意味着必须在失败时抛出,或者对象不能处于空状态,例如,unique_ptr可以用空指针构造或默认构造,因此也可以指向任何东西,因此在某些情况下,您需要在取消引用之前检查它。

文件流在构建时获取资源,并在销毁时释放它--就我而言,这是RAII。您反对的是要求检查,这闻起来像是两个阶段的初始化,我同意这有点臭。不过,这并不意味着它不会下雨。

在过去,我用CheckedFstream类解决了这个问题,这个类是一个简单的包装器,它添加了一个功能:如果流无法打开,则抛出contrator。在C++11中,这很简单:

struct CheckedFstream : std::fstream
{
  CheckedFstream() = default;

  CheckedFstream(std::string const& path, std::ios::openmode m = std::ios::in|std::ios::out)
  : fstream(path, m)
  { if (!is_open()) throw std::ios::failure("Could not open " + path); }
};

相关文章