【问题标题】:Why do C++ standard file streams not follow RAII conventions more closely?为什么 C++ 标准文件流不更紧密地遵循 RAII 约定?
【发布时间】:2014-10-26 19:12:35
【问题描述】:

为什么 C++ 标准库流使用与对象生命周期分离的open()/close() 语义?在销毁时关闭可能在技术上仍会使类成为 RAII,但获取/释放独立性会在句柄可以指向任何内容但仍需要运行时检查才能捕获的范围中留下漏洞。

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

void foo() {
  std::ofstream ofs;
  ofs << "Can't do this!\n"; // 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!\n"; // 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 值得拥有。通过句柄生命周期控制打开文件的持续时间在我看来根本不是一种负担,但实际上是一种安全优势。

【问题讨论】:

  • 是的,它肯定缺少throw_exception 模式值。可以为后面的操作设置异常,但抛出构造函数会更好。

标签: c++ iostream raii c++-standard-library


【解决方案1】:

虽然其他答案都是有效且有用的,但我认为真正的原因更简单。

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

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

对于文件,您可能需要检查close() 调用是否成功,以便您知道数据是否已写入磁盘,这是在析构函数中执行此操作的充分理由,因为当对象被销毁时,对它做任何有用的事情都为时已晚,而且您几乎肯定不想从析构函数中抛出异常。因此fstreambuf 将在其析构函数中调用 close,但如果您愿意,您也可以在销毁之前手动执行此操作。

无论如何,我不同意它不遵循 RAII 约定...

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

注意RAII 并不意味着您不能除了获取资源的构造函数之外还有一个单独的open() 成员,或者您不能在销毁之前清理资源例如unique_ptr 有一个 reset() 成员。

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

文件流在构造时获取资源并在销毁时释放它 - 就我而言,这就是 RAII。你反对的是需要检查,这有两阶段初始化的味道,我同意这有点臭。但这并不意味着它不是 RAII。

过去我用CheckedFstream 类解决了这种气味,它是一个简单的包装器,添加了一个功能:如果无法打开流,则抛出 cosntructor。在 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); }
};

【讨论】:

  • 关于为什么异常不是默认的有趣事实。
  • 是的,'not' 是错字,现在已修复。此外,我一直假设单阶段初始化是 RAII 理念的核心部分,而不仅仅是一种互补的设计方法。
  • fstreams不需要两阶段init,你可以调用构造函数,也不必检查is_open(),你可以开始写。如果文件未打开,这将失败(并设置failbit,并且可能会抛出,具体取决于异常掩码)。因此,您可以以正常的一阶段初始化方式使用它,如果需要,它将在销毁时进行清理。这是 RAII 恕我直言的有效形式。
  • @JonathanWakely 我实际上已经使用了几个与我自己几乎没有区别的包装器。不过,我确实对您检查过的close() 评论有疑问:当时真的有可能在ofstream 上执行任何有意义的事情,或者事先检查flush 不能更好地服务吗?我从未亲眼目睹有人检查close(),我想知道这只是普遍的冷漠还是更深层次的复杂性。
  • 另外,关于两阶段初始化:两阶段初始化的对象是它可以使对象处于不可用状态,必须测试。这个反对意见不适用于 IO(也不适用于支持空指针值的智能指针),因为它们可能在任何时间点变得不可用,即使在成功构造之后也是如此。所以无论如何你都必须在任何地方进行测试。
【解决方案2】:

这样你会得到更多而不是更少。

  • 您得到相同:您仍然可以通过构造函数打开文件。你仍然会得到 RAII:它会在对象销毁时自动关闭文件。

  • 您会得到更多:您可以使用相同的流重新打开其他文件;您可以在需要时关闭文件,而不是被限制等待对象超出范围或被破坏(这非常重要)。

  • 您会得到一点也不差:您看到的优势并不真实。您说您不必在每次操作时检查。这是错误的。即使成功打开(文件),流也可能随时失败。

关于错误检查与抛出异常,请参阅@PiotrS’s answer。从概念上讲,我认为必须检查返回状态与必须捕获错误之间没有区别。错误仍然存​​在;不同之处在于您如何检测它。但正如@PiotrS 所指出的,您可以同时选择两者。

【讨论】:

  • std::ios::exceptions() 掩码允许未经检查的错误传播,如 Piotr 所述。
  • @Jeff 我更多地解释了这个问题,为什么你可以随时明确地打开/关闭流;而不是为什么错误检查而不是异常。
  • 更多的是关于开/关方面,我想我仍然不同意你更多/更少的断言。进行声明/打开然后检查或声明,异常掩码,然后打开比抛出失败的实例化更笨拙。我还质疑具有重定向文件句柄的能力是否真的能买到任何东西。用户空间句柄构造/销毁的成本与底层系统调用相比相形见绌,并且 descoping==closure 可以防止一小部分愚蠢的访问错误。
  • @Jeff 在某些情况下,保持文件处理程序处于打开状态会阻止其他进程打开该文件。能够随时关闭句柄而不是等待对象销毁是至关重要的。至于重定向我同意,不是一个很大的优势,但你仍然可以选择。
【解决方案3】:

图书馆设计师为您提供了替代方案:

std::ifstream file{};
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);

try
{
    file.open(path); // now it will throw on failure
}
catch (const std::ifstream::failure& e)
{
}

【讨论】:

  • 这是一个很好的观点,但它仍然使异常屏蔽与实例创建分离,并保留了访问必须在运行时捕获的未打开文件的可能性。
  • @Jeff:似乎我根本不明白你的问题,关闭无效打开的流不会导致任何问题:“如果操作失败(包括在调用之前没有打开文件),则故障位状态为流设置了标志“
  • 对不起,我的意思是在关闭或非打开构造之后打开之前对文件流的访问永远不会起作用,但是流状态(或启用的异常)必须触发运行-time 错误,而不是使用作用域变量生存期来防止它在编译时发生。
【解决方案4】:

标准库文件流提供RAII,在 感觉调用析构函数将关闭任何文件 恰好是开放的。至少在输出的情况下, 但是,这是紧急措施,只能使用 如果您遇到另一个错误,并且不打算使用 无论如何正在写入的文件。 (好的编程 实践是删除它。)通常,您需要检查 流你关闭它之后的状态,这是 一个可能失败的操作,所以不应该在 析构函数。

对于输入,它并不那么重要,因为您已经检查了 无论如何,最后一次输入后的状态,大多数时候,会 一直读到输入失败。但这似乎是合理的 两者具有相同的界面;从编程的角度来看 但是,您通常可以让 close 在 析构函数根据输入完成其工作。

关于open:您可以轻松地打开 构造函数,对于您展示的独立用途,这是 可能是首选解决方案。但是在某些情况下,您 可能想要重用std::filebuf,打开并关闭它 明确地,当然,在几乎所有情况下,您都希望 处理无法立即打开文件,而不是 通过一些例外。

【讨论】:

    【解决方案5】:

    这取决于你在做什么,阅读或写作。 您可以以 RAII 方式封装输入流,但对于输出流则不然。如果目标是磁盘文件或网络套接字,永远不要将 fclose/close 放入析构函数中。因为需要检查fclose的返回值,而析构函数发生错误是没有办法报错的。见How can I handle a destructor that fails

    【讨论】:

      猜你喜欢
      • 2017-05-28
      • 2011-10-07
      • 2010-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多