RAII与例外

Ass*_*vie 48 c++ destructor exception raii

我们在C++中使用RAII的次数越多,我们就越发现自己的析构函数会进行非平凡的释放.现在,解除分配(终结,但是你想要调用它)可能会失败,在这种情况下,异常实际上是让楼上的任何人知道我们的释放问题的唯一方法.但是再说一次,抛出析构函数是一个坏主意,因为在堆栈展开期间可能会抛出异常.std::uncaught_exception()让你知道什么时候发生,但不是更多,所以除了让你在终止之前记录一条消息之外你没有太多可以做的,除非你愿意让你的程序处于未定义的状态,其中一些东西被解除分配/最终化而一些不是.

一种方法是使用无抛出析构函数.但在许多情况下,这只是隐藏了一个真正的错误.例如,我们的析构函数可能会因为抛出某些异常而关闭一些RAII管理的数据库连接,并且这些数据库连接可能无法关闭.这并不一定意味着我们可以在此时终止程序.另一方面,记录和跟踪这些错误并不是每个案例的真正解决方案; 否则我们就不需要开始例外了.使用无抛出析构函数,我们还发现自己必须创建应该在销毁之前调用的"reset()"函数 - 但这只会破坏RAII的整个目的.

另一种方法是让程序终止,因为这是你可以做的最可预测的事情.

有些人建议链接异常,以便一次可以处理多个错误.但老实说,我从来没有真正看到用C++完成的工作,我也不知道如何实现这样的东西.

所以它是RAII或例外.不是吗?我倾向于无抛出的破坏者; 主要是因为它保持简单(r).但我真的希望有一个更好的解决方案,因为,正如我所说,我们使用RAII的次数越多,我们发现自己越多地使用执行非平凡事情的dtors.

附录

我正在添加链接到我发现的有趣的主题文章和讨论:

Mar*_*ork 18

不应该从析构函数中抛出异常.

注意:已更新以更新标准中的更改:

在C++ 03中
如果异常已经传播,则应用程序将终止.

在C++ 11中
如果析构函数是noexcept(默认值),那么应用程序将终止.

以下是基于C++ 11

如果异常转义noexcept函数,则在堆栈甚至解除时,它是实现定义的.

以下是基于C++ 03

终止,我的意思是立即停止.堆栈展开停止.不再需要析构函数.所有不好的东西.请参阅此处的讨论.

从析构函数中抛出异常

我不遵循(如不同意)你的逻辑,这会导致析构函数变得更复杂.
通过正确使用智能指针,这实际上使析构函数更简单,因为现在所有内容都变为自动化.每个班级都会将自己的小部分拼凑出来.这里没有脑部手术或火箭科学.RAII的另一大胜利.

至于std :: uncaught_exception()的可能性,我指出Herb Sutters关于它为什么不起作用的文章

  • 我从未说过密集故障的可能性会导致故障变得更加复杂.RAII导致了这一点.你越多地使用dtors来拆除东西,你就越有可能在拆除时遇到错误. (5认同)
  • 我同意阿萨夫的观点。您在 dtors 中放置了更多(清理)内容,以便在抛出异常时执行它,但这种情况的退化情况是一个只有 ctors 和(隐式)dtors 的函数。所以 dtors 中的很多东西 -> 是很多例外的机会。 (2认同)

Aar*_*ron 8

从原来的问题:

现在,解除分配(终结,但是你想要调用它)可能会失败,在这种情况下异常实际上是让楼上的任何人知道我们的释放问题的唯一方法

无法清理资源或者表明:

  1. 程序员错误,在这种情况下,您应该记录失败,然后通知用户或终止应用程序,具体取决于应用程序方案.例如,释放已经释放的分配.

  2. 分配器错误或设计缺陷.请参阅文档.错误很可能有助于诊断程序员错误.见上文第1项.

  3. 否则不可恢复的不利条件可以继续.

例如,C++免费存储具有无失败运算符删除.其他API(如Win32)提供错误代码,但只会因程序员错误或硬件故障而失败,错误指示堆损坏或双重释放等情况.

至于不可恢复的不利条件,请采用DB连接.如果关闭连接失败,因为连接被删除 - 很酷,你就完成了.不要扔!断开的连接(应该)导致关闭连接,因此不需要做任何其他事情.如果有,请记录跟踪消息以帮助诊断使用问题.例:

class DBCon{
public:
  DBCon() { 
    handle = fooOpenDBConnection();
  }
  ~DBCon() {
    int err = fooCloseDBConnection();
    if(err){
      if(err == E_fooConnectionDropped){
        // do nothing.  must have timed out
      } else if(fooIsCriticalError(err)){
        // critical errors aren't recoverable.  log, save 
        //  restart information, and die
        std::clog << "critical DB error: " << err << "\n";
        save_recovery_information();
        std::terminate();
      } else {
        // log, in case we need to gather this info in the future,
        //  but continue normally.
        std::clog << "non-critical DB error: " << err << "\n";
      }
    }
    // done!
  }
};
Run Code Online (Sandbox Code Playgroud)

这些条件都没有理由尝试第二种放松.程序可以正常继续(包括异常展开,如果正在进行展开),或者它在此处和现在都会消失.

编辑 - 添加

如果你真的希望能够保持与那些无法关闭的数据库连接的某种链接 - 也许它们由于间歇性条件而无法关闭,而你想稍后重试 - 那么你总是可以推迟清理:

vector<DBHandle> to_be_closed_later;  // startup reserves space

DBCon::~DBCon(){
  int err = fooCloseDBConnection();
  if(err){
    ..
    else if( fooIsRetryableError(err) ){
      try{
        to_be_closed.push_back(handle);
      } catch (const bad_alloc&){
        std::clog << "could not close connection, err " << err << "\n"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

非常不漂亮,但它可能会为你完成工作.

  • 您的样式假定您的对象使用terminate()知道应用程序的最佳选择.这种情况几乎从来没有(从未读过),周围的代码(即控制代码)具有决定如何处理错误的上下文. (3认同)
  • 在许多情况下,清理失败后的正确行为既不"继续好像没有错",也不"立即杀死整个系统".例如,"保存文档"方法应抛出异常,除非一切正常*包括close*.如果保存文档需要同时写入两个文件并且在"SaveDocument"方法期间拔出USB驱动器,并且对一个文件的写入会抛出异常(可能); 另一个文件的析构函数也会失败.让应用程序立即终止会非常粗鲁,但...... (2认同)

pae*_*bal 6

这让我想起了一位同事在向他解释异常/RAII 概念时提出的一个问题:“嘿,如果计算机关闭,我可以抛出什么异常?”

不管怎样,我同意 Martin York 的回答RAII vs. 异常

异常和析构函数有什么关系?

许多 C++ 功能都依赖于非抛出析构函数。

事实上,RAII 的整个概念及其与代码分支(返回、抛出等)的配合是基于释放不会失败的事实。同样,当您想为对象提供高异常保证时,某些函数不应该失败(例如 std::swap)。

但这并不意味着您不能通过析构函数抛出异常。只是该语言甚至不会尝试支持这种行为。

如果获得授权会发生什么?

只是为了好玩,我试着想象一下......

如果你的析构函数无法释放你的资源,你会做什么?您的对象可能已被毁坏一半,您会从“外部”捕获该信息做什么?再试一次?(如果是,那么为什么不在析构函数中再次尝试?...)

也就是说,如果您无论如何都可以访问半破坏的对象:如果您的对象位于堆栈上(这是 RAII 工作的基本方式)怎么办?如何访问超出其范围的对象?

在异常中发送资源?

您唯一的希望是在异常内发送资源的“句柄”,并希望代码在 catch 中,好吧......再次尝试释放它(见上文)?

现在,想象一些有趣的事情:

 void doSomething()
 {
    try
    {
       MyResource A, B, C, D, E ;

       // do something with A, B, C, D and E

       // Now we quit the scope...
       // destruction of E, then D, then C, then B and then A
    }
    catch(const MyResourceException & e)
    {
       // Do something with the exception...
    }
 }
Run Code Online (Sandbox Code Playgroud)

现在,让我们想象一下由于某种原因 D 的析构函数未能释放资源。您对其进行编码以发送异常,该异常将被 catch 捕获。一切都很顺利:你可以按照你想要的方式处理失败(我仍然不知道如何以建设性的方式处理失败,但现在这不是问题)。

但...

在多个异常中发送多个资源?

现在,如果 ~D 可能失败,那么 ~C 也可能失败。以及〜B和〜A。

在这个简单的示例中,您有 4 个析构函数在“同一时刻”失败(退出作用域)。您需要的不是捕获一个异常,而是捕获一组异常(希望为此生成的代码不会...呃...抛出)。

    catch(const std::vector<MyResourceException> & e)
    {
       // Do something with the vector of exceptions...
       // Let's hope if was not caused by an out-of-memory problem
    }
Run Code Online (Sandbox Code Playgroud)

让我们重新开始(我喜欢这个音乐...):每个抛出的异常都是不同的(因为原因不同:请记住,在 C++ 中,异常不需要从 std::exception 派生)。现在,您需要同时处理四个异常。如何编写 catch 子句来按类型和抛出顺序处理四个异常?

如果您有多个相同类型的异常,这些异常是由多次失败的释放引发的,该怎么办?如果在分配异常数组的内存时,您的程序内存不足,并且,呃...抛出内存不足异常,该怎么办?

您确定要花时间解决此类问题,而不是花时间思考释放失败的原因或如何以其他方式对此做出反应吗?

显然,C++ 设计者没有看到可行的解决方案,只是止损于此。

问题不在于 RAII 与异常......

不,问题是有时候,事情可能会失败太多,以至于无能为力。

只要满足某些条件,RAII 就可以很好地处理异常。其中:析构函数不会抛出. 您所看到的反对派只是结合了两个“名称”的单一模式的一个极端情况:ExceptionRAII

当析构函数出现问题时,我们必须接受失败,并挽救能挽救的东西:“数据库连接无法释放?抱歉。让我们至少避免这种内存泄漏并关闭这个文件。”

虽然异常模式(应该是)C++ 中的主要错误处理,但它并不是唯一的一种。当 C++ 异常不是解决方案时,您应该通过使用其他错误/日志机制来处理异常(双关语)情况。

因为你刚刚遇到了语言中的一堵墙,在我所知道或听说过的其他语言中,没有哪一堵墙能够正确穿过而不会导致房屋倒塌(C# 的尝试是值得的,而 Java 的尝试仍然是一个让我感到受伤的笑话) ...我什至不会谈论会以同样无声的方式在同一问题上失败的脚本语言)。

但最终,无论您编写多少代码,您都不会受到用户关闭计算机的保护

你能做的最好的事情,你已经写好了。我自己的偏好是使用抛出终结方法、非抛出析构函数来清理未手动完成的资源,以及日志/消息框(如果可能)来警告析构函数中的失败。

也许你没有进行正确的决斗。而不是“RAII 与异常”,它应该是“尝试释放资源与绝对不想被释放的资源,即使受到破坏的威胁

:-)

  • C++ 无法支持该行为的根本原因是没有通用的异常基类型。如果有一个公共基类型,其中包含获取“child”和“sibling”指针的属性,以及“should_catch&lt;T&gt;”和“is_resolved”的方法,那么处理多个异常应该是相当可行的。如果任何*待处理的异常满足“Foo”,则应运行“catch Foo”;在“catch”之后,如果仍有任何未解决的异常,系统应继续展开。 (2认同)

Ste*_*sop 6

你在看两件事:

  1. RAII,保证在退出作用域时清理资源。
  2. 完成一个操作并查看它是否成功。

RAII 承诺它将完成操作(释放内存,关闭尝试刷新的文件,结束尝试提交的事务)。但是因为它是自动发生的,不需要程序员做任何事情,它不会告诉程序员它“尝试”的那些操作是否成功。

异常是报告某事失败的一种方式,但正如您所说,C++ 语言有一个限制,这意味着它们不适合从析构函数 [*] 执行此操作。返回值是另一种方式,但更明显的是析构函数也不能使用它们。

因此,如果您想知道您的数据是否已写入磁盘,则不能使用 RAII。它不会“破坏 RAII 的全部目的”,因为 RAII 仍会尝试写入它,并且仍会释放与文件句柄(DB 事务,无论如何)关联的资源。它确实限制了 RAII 可以做什么——它不会告诉您数据是否已写入,因此您需要一个close()可以返回值和/或抛出异常的函数。

[*] 这也是一个很自然的限制,存在于其他语言中。如果您认为RAII析构函数应该抛出异常说“出了问题!”,然后什么都有时,已经是在飞行中的例外,那就是发生“别的东西出了问题,甚至在那之前!”。我知道使用异常的语言不允许同时出现两个异常——语言和语法根本不允许这样做。如果 RAII 要执行您想要的操作,则需要重新定义异常本身,以便一个线程一次出现不止一个问题,并且两个异常向外传播并调用两个处理程序,一个来处理。

其他语言允许第二个异常掩盖第一个异常,例如,如果finally块在 Java 中抛出。C++ 几乎说必须抑制第二个,否则terminate调用(在某种意义上同时抑制两者)。在这两种情况下,更高的堆栈级别都不会通知两个错误。有点不幸的是,在 C++ 中,您无法可靠地判断一个异常是否过多(uncaught_exception不会告诉您,它会告诉您一些不同的东西),因此您甚至无法抛出已经不是飞行中的例外了。但即使在那种情况下你能做到,你仍然会被一个多一个的情况塞满。