C++ RAII 析构函数 异常

4
据我了解,RAII 是指在构造函数中获取资源,在析构函数中释放资源。
构造函数获取某些资源时可能会失败,导致异常抛出。 析构函数释放资源时也可能会失败,但出现在析构函数中的异常不能被处理,因此不允许发生异常。
class A {
  A() throw(Ex) { // acquire resources }
  ~A() throw() { // release resources }
}

如果A类的用户应该知道在A的未初始化过程中发生了错误,那么我可以将未初始化过程外包给一个抛出异常的函数,在析构函数中调用该函数并吞掉异常:

class A {
  A() throw(Ex) { // acquire resources }
  ~A() throw() { try {Release(); } catch(...) {} }

  void Release() throw(Ex) { // release resources }
}

那么用户可以在需要释放错误反馈时调用Exit(),或者当A超出作用域时,让dtor完成工作(例如,A在发生其他异常的情况下被使用)。

为了防止多次执行Exit()(首先由用户明确执行,稍后由dtor间接执行),我必须添加一个初始状态:

class A {
  bool init;
  A() throw(Ex) { init = true; // acquire resources }
  ~A() throw() { try {Release(); } catch(...) {} }

  void Release() throw(Ex) {
    if(!init) return;
    init = false;
    // release resources
   }
}

有没有更好的方法可以做到这一点,或者每次资源释放失败并且我想知道时,我都必须实现该模式?


3
作为一个经验法则:不要从析构函数中抛出异常! - πάντα ῥεῖ
2
结果是RAII不处理释放过程中的错误。因此,您应该选择非抛出式释放的合理默认行为,并在需要时添加显式错误处理机制。 std :: fstream 就这样做了。 - Kerrek SB
你不应该使用除了指定函数不会抛出异常(这可能会使用noexceptnoexcept(true))之外的异常规范。 - Dietmar Kühl
在这种情况下,使用RAII时,错误处理机制总是必须放置在类本身而不是使用它的地方? - downforme
我知道编译器会忽略throw(Ex)子句,这只是为了解释类的行为。 - downforme
2个回答

3
释放资源不应该有失败的可能性。例如,释放内存可以以不抛出异常的形式实现。 RAII 的目的是清理资源而不是处理由于大量清理导致的错误。
显然,有些清理操作可能会失败。例如,关闭文件可能会失败,例如因为关闭它将刷新内部缓冲区,并且由于写入文件的磁盘已满,这可能会失败。如果清理操作失败,则可能需要适当的释放操作,如果用户有兴趣报告清理过程中的错误,则应使用此方法:在正常路径中,将有机会处理任何错误。
当释放作为处理现有错误的一部分时,即抛出异常并且未到达释放操作时,任何异常都需要被析构函数吞噬。可能有一些处理方法,例如记录抛出的异常消息,但异常不应逃脱析构函数。

  1. 释放资源有时可能会失败。假设你使用某些第三方库,并且它有 foo_status release_foo(foo_handle_type fh),你该怎么办?
  2. 析构函数不能合理地处理像记录日志这样的事情。应用程序知道如何在何处记录日志,而不是某个 RAII 资源持有类(可能不特定于应用程序)。
- einpoklum
多么傲慢的回答。资源释放和资源分配一样容易失败。 - Enerccio

2

总的来说,如果您一直遵循RAII指南,那么在析构函数中肯定需要抛出异常。

例如:关闭文件、释放互斥锁、关闭套接字连接、取消映射文件、关闭通信端口、回滚数据库事务等。在RAII销毁中完成过多的工作将会失败。

我们应该如何处理这些失败呢?在RAII析构函数中,我们几乎没有足够的信息来知道如何正确地处理这些失败。我们只能选择忽略它或将其传递到上层。

但是,如果这些错误可以安全地忽略,为什么操作系统提供的API(如close、munmap、pthread_mutex_destroy等)都会向我们返回错误代码呢?难道它们不能简单地返回void吗?

因此,我们最终不得不编写这样的析构函数:

CResource::~CResource() noexcept(false)
{
    if (-1 == close(m_fd))
    {
        // ...
        if (std::uncaught_exception())
        {
            return;
        }
        throw myExp(m_fd, ...);
    }
    // ...
}

当然,除了抛出异常之外,我们还可以选择自己的向上传递方法。例如,让上层组件为每种可能在析构时抛出的类型注册回调方法,或者维护一个全局队列来存储和传递这些异常等等。
但是很明显,这些替代方案更加笨拙和难以使用。这相当于重新实现异常机制。

std::uncaught_exception() 是一个不好的想法。至少使用 std::uncaught_exceptions()。注意 s - Deduplicator
@Deduplicator 谢谢,但坦率地说,在我们的情况下,我没有发现使用 s 版本有任何额外的优势? - ASBai
@Deduplicator 如果我理解正确的话,你的意思是一些析构函数可能会暂时构造一些对象,并且这些临时对象的析构函数仍然可以抛出异常,对吗?这可能是有用的,但我认为在析构函数中构造一个新对象不是一个好主意。 - ASBai
在析构函数中创建对象比从其中抛出更安全。您是否曾经在析构函数中记录任何内容?这也会根据您的日志记录方式创建临时对象。许多标准库依赖于析构函数永远不会抛出异常,因为它会破坏提交或回滚。 - Deduplicator
但是创建临时对象并不能帮助避免抛出异常。需要抛出的异常仍然需要被抛出,而临时对象只会增加另一个析构函数,使问题变得更加复杂。你使用的日志工具是什么?我使用的日志工具在添加新日志记录时不需要创建任何可能在解构时抛出异常的对象。事实上,我使用的记录器只需要在添加新日志时创建一个字符串对象。 - ASBai
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接