【问题标题】:c++ RAII destructor exceptionc++ RAII析构函数异常
【发布时间】:2014-07-06 11:29:29
【问题描述】:

据我了解,RAII是指在ctor中获取资源并在dtor中释放。

Ctor 获取了一些资源并且可能会失败,从而导致异常。 Dtor 释放资源也可能失败,但来自 dtor 的异常是 foobar 所以也不例外。

class A {
  A() throw(Ex) { // acquire resources }
  ~A() throw() { // release resources }
}

因此,如果应该让 A 类的用户意识到 A 的未初始化中发生的错误,我可以将未初始化外包给一个抛出的函数,该函数从一个吞下异常的 dtor 调用:

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

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

这样,如果用户想要发布错误的反馈,用户可以调用 Exit(),或者在 A 超出范围时让 dtor 完成它的工作而忽略(例如,在使用 A 的地方会发生一些其他异常)。

为了防止多次执行 Exit()(首先从用户显式执行,然后由 dtor 间接执行),我必须添加一个 init-status:

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
   }
}

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

【问题讨论】:

  • 根据经验:不要从析构函数中抛出异常!
  • 结果是RAII不处理发布过程中的错误。因此,您应该为非抛出版本选择一个合理的默认行为,并在需要时添加一个明确的错误处理机制。 std::fstream 这样做。
  • 除了指定一个函数不抛出外,您是否不使用异常规范(这可能会使用noexceptnoexcept(true))。
  • 在这种情况下,对于 RAII,错误处理机制总​​是必须放在类本身而不是使用它的地方?
  • 我知道编译器忽略了 throw(Ex) 子句,它只是为了解释类的行为

标签: c++ exception destructor raii


【解决方案1】:

释放资源不应该有任何失败的余地。例如,释放内存当然可以以不抛出异常的形式实现。 RAII 旨在清理资源,而不是处理大量清理导致的错误。

显然,有些清理操作可能会失败。例如,关闭文件可能会失败,例如,因为关闭它会刷新内部缓冲区,而这可能会失败,因为文件正在写入的磁盘已满。如果清理操作失败,可能应该有一个合适的释放操作,如果用户有兴趣从清理报告错误,他们应该使用这种方法:在正常路径中,有机会处理任何错误.

当释放作为处理现有错误的一部分进行时,即抛出异常并且未达到释放操作时,析构函数将需要处理任何异常。可能有一些处理方法,例如记录抛出异常的消息,但异常不应该逃脱析构函数。

【讨论】:

    【解决方案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 谢谢,但坦率地说,我没有发现在我们的场景中使用 s 版本有任何额外的优势?
    • 问题是你无法区分在异常运行时被使用和由于异常导致堆栈展开而被调用。
    • @Deduplicator 如果我理解正确的话,你的意思是一些析构函数可能会临时构造一些对象,而这些临时对象的析构函数仍然可以抛出异常对吧?它可能有用,但我认为在析构函数中构造新对象不是一个好主意。
    • 在析构函数中创建对象比从析构函数中抛出要安全得多。你有没有在析构函数中记录过任何东西?这会创建临时对象,具体取决于您的日志记录方式。大部分标准库都依赖于不抛出的析构函数,因为它破坏了提交或回滚。
    猜你喜欢
    • 1970-01-01
    • 2013-10-01
    • 1970-01-01
    • 2013-01-20
    • 2017-08-16
    • 2012-04-11
    • 1970-01-01
    • 2016-07-21
    • 2022-01-16
    相关资源
    最近更新 更多