RAII与例外

时间:2020-03-06 14:59:35  来源:igfitidea点击:

我们在C ++中使用RAII的次数越多,发现进行非平凡释放的析构函数的机会就越多。现在,重新分配(最终确定,但是我们要称呼它)可能会失败,在这种情况下,异常实际上是让楼上的所有人知道我们重新分配问题的唯一方法。但是再说一次,抛出析构函数是一个坏主意,因为在堆栈展开期间可能会抛出异常。 std :: uncaught_exception()告诉我们什么时候发生,但是不多,因此除了让我们在终止之前记录消息外,我们无能为力,除非我们愿意让程序保持未定义状态,其中一些东西被释放/完成,而另一些则没有。

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

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

有人建议链接异常,以便一次可以处理多个错误。但老实说,我从来没有真正看到过用C ++做到这一点,而且我也不知道如何实现这样的事情。

所以它是RAII或者例外。是不是我倾向于不丢球的破坏者。主要是因为它使事情变得简单。但是我真的希望有一个更好的解决方案,因为正如我说的那样,我们使用RAII的次数越多,我们发现使用做非琐碎事情的dtor就会越多。

附录

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

  • 投掷破坏者
  • 关于SEH问题的StackOverflow讨论
  • StackOverflow关于抛出析构函数的讨论(感谢Martin York)
  • 乔尔的例外
  • SEH被认为有害
  • CLR异常处理也涉及异常链接
  • 在std :: uncaught_exception上的Herb Sutter以及为什么它没有我们想象的有用
  • 与有趣的参与者就此事进行历史讨论(很长!)
  • Stroustrup解释RAII
  • 安德烈·亚历山大(Andrei Alexandrescu)的瞄准镜

解决方案

我们不应该从析构函数中抛出异常。

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

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

在C ++ 11中
如果析构函数为" noexcept"(默认值),则应用程序将终止。

以下基于C ++ 11

如果异常转义了一个" noexcept"函数,则即使堆栈未展开,也将由实现定义。

以下基于C ++ 03

终止是指立即停止。堆栈放卷停止。没有更多的析构函数被调用。所有的坏东西。请参阅此处的讨论。

从析构函数中抛出异常

我不遵循(不同意)逻辑,因为这会使析构函数变得更加复杂。
通过正确使用智能指针,这实际上使析构函数更简单,因为现在一切都变得自动化了。每个班级拼凑出自己的一小块拼图。这里没有脑外科手术或者火箭科学。 RAII的另一个重大胜利。

至于std :: uncaught_exception()的可能性,我指出了Herb Sutters文章中有关其为何不起作用的内容

我们可以通过检查以下内容来判断当前是否正在运行异常(例如,我们处于throw和catch块之间,正在执行堆栈展开,可能正在复制异常对象或者类似对象)

bool std::uncaught_exception()

如果返回true,则此时抛出将终止程序;如果不返回,则可以安全地抛出(或者至少与以往一样安全)。 ISO 14882(C ++标准)的15.2和15.5.3节对此进行了讨论。

这不能回答在清理异常时遇到错误时该怎么办的问题,但是实际上并没有任何好的答案。但是,如果我们在后一种情况下等待执行其他操作(例如,记录并忽略它),而不是仅仅因为惊慌,那么它确实使我们能够区分正常退出和异常退出。

从最初的问题:

Now, deallocation (finalization,
  however you want to call it) can fail,
  in which case exceptions are really
  the only way to let anybody upstairs
  know of our deallocation problem

无法清除资源或者表明:

  • 程序员错误,在这种情况下,应记录故障,然后通知用户或者终止应用程序,具体取决于应用程序方案。例如,释放已经释放的分配。
  • 分配器错误或者设计缺陷。请查阅文档。错误很可能在那里可以帮助诊断程序员错误。请参阅上面的项目1.
  • 否则无法恢复的不利情况可以继续。

例如,C ++免费商店有一个不失败的运算符删除。其他API(例如Win32)提供错误代码,但只会由于程序员错误或者硬件故障而失败,错误指示堆损坏或者double free等情况。

对于无法恢复的不利条件,请进行数据库连接。如果由于连接掉线而导致关闭连接失败-cool,则说明我们已经完成了。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。不要扔!断开的连接(应该)会导致连接关闭,因此无需执行其他任何操作。如果有的话,请记录跟踪消息以帮助诊断使用问题。例子:

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

这些条件都不能证明尝试第二种放松。程序可以正常继续运行(包括异常展开,如果展开正在进行中),或者该程序在此处和现在消失。

编辑添加

如果我们确实希望能够与无法关闭的那些数据库连接保持某种联系-也许是由于间歇性条件而无法关闭,并且我们想稍后重试-那么我们可以始终推迟清理:

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

非常不漂亮,但这可能会为我们完成工作。

我想问的一件事是,忽略终止等问题,我们认为适当的响应是,如果程序由于正常破坏或者异常破坏而无法关闭其数据库连接。

我们似乎排除了"仅进行日志记录"并且不愿意终止,那么我们认为最好的做法是什么?

我认为,如果我们对该问题有一个答案,那么我们将对如何进行有更好的了解。

在我看来,没有什么策略特别明显。除了别的什么,我真的不知道关闭数据库连接抛出的含义。如果close()抛出,连接的状态是什么?它是封闭的,仍打开的还是不确定的?如果不确定,程序是否有任何方法可以还原到已知状态?

析构函数的失败意味着没有办法撤销对象的创建。使程序返回到已知(安全)状态的唯一方法是拆除整个过程并重新开始。

销毁可能失败的原因是什么?为什么不在实际破坏之前考虑处理这些问题呢?

例如,关闭数据库连接可能是因为:

  • 交易正在进行中。 (检查std :: uncaught_exception()-如果为true,回滚或者其他commit-这些是最可能需要执行的操作,除非我们在实际关闭连接之前有另外说的策略。)
  • 连接已断开。 (检测并忽略。服务器将自动回滚。)
  • 其他数据库错误。 (记录下来,以便我们将来进行调查并可能进行适当的处​​理。这可能是检测到并忽略。与此同时,请尝试回滚并再次断开连接,并忽略所有错误。)

如果我正确理解RAII(我可能不会),那么重点就是它的范围。因此,这与我们想要的事务持续时间长于对象相比并不更长。因此,对我来说,我们想确保尽可能做到封闭是合情合理的。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。即使根本没有对象,RAII也无法做到这一点的独特性(例如C语言),我们仍然会尝试捕获所有错误条件,并尽最大可能处理它们(有时会忽略它们)。 RAII所做的一切就是迫使我们将所有代码放在一个地方,无论使用该资源类型的函数有多少。

当我向他解释异常/ RAII概念时,它使我想起了一个同事的问题:"嘿,如果计算机关闭,我可以抛出什么异常?"

无论如何,我同意Martin York的回答RAII与例外

异常和析构函数如何处理?

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

实际上,RAII的整个概念及其与代码分支(返回,抛出等)的协作都是基于这样的事实,即重新分配不会失败。以同样的方式,当我们要为对象提供高异常保证时,某些功能也不应失败(例如std :: swap)。

这并不是说我们不能通过析构函数抛出异常。只是该语言甚至不会尝试支持这种行为。

如果它被授权会怎样?

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

如果析构函数无法释放资源,我们将怎么办?对象可能被破坏了一半,从该信息的"外部"捕获中我们将如何处理?再试一次? (如果是,那么为什么不从析构函数中再次尝试?...)

也就是说,如果我们无论如何都可以访问半毁灭的对象:如果对象在堆栈上(那是RAII的基本工作方式)怎么办?如何访问超出其范围的对象?

在异常内发送资源?

我们唯一的希望是在异常内发送资源的"句柄",并希望代码在捕获中,好吧...再尝试重新分配它(参见上文)?

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

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

现在,让我们想象一下由于某种原因,D的析构函数无法释放资源。我们对它进行了编码以发送一个异常,该异常将被捕获捕获。一切顺利:我们可以按自己想要的方式来处理失败(以建设性的方式仍无法解决我的问题,但是现在,这已不是问题)。

但...

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

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

在这个简单的示例中,我们有4个析构函数,它们在"相同时刻"失败(退出范围)。我们所需的不是不是具有一个异常的捕获,而是具有一系列异常的捕获(我们希望为此生成的代码不会... er ... throw)。

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
    }

让我们推迟一下(我喜欢这种音乐...):抛出的每个异常都是一个不同的(因为原因不同:请记住,在C ++中,异常不必从std :: exception派生)。现在,我们需要同时处理四个异常。我们如何编写catch子句,以它们的类型以及抛出的顺序来处理这四个异常?

而且,如果我们有多个相同类型的异常被多个失败的重新分配抛出,该怎么办?而且,如果在分配数组的异常数组的内存时,程序耗尽了内存,并且……抛出了内存不足的异常,该怎么办?

我们确定要花时间在这种问题上,而不是花在弄清分配失败的原因或者如何以其他方式做出反应上吗?

显然,C ++设计人员没有看到可行的解决方案,只是在这里减少了损失。

问题不在于RAII与例外...

不,问题是有时候事情会失败很多,无能为力。

只要满足某些条件,RAII即可很好地与Exceptions配合使用。其中:破坏者不会抛出。我们所看到的是一个对立,只是一个模式的一个极端情况,该模式结合了两个"名称":Exception和RAII

如果析构函数中出现问题,我们必须接受失败,然后挽救可以挽救的东西:"数据库连接未能解除分配?对不起。让我们至少避免这个内存泄漏并关闭这个文件。"

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

因为我们只是碰到一堵墙的语言,所以没有一堵墙我能听到或者听到的其他语言都能正确通过而不会倒塌(Cattempt是值得的,而Java的墙仍然是一个玩笑,这对我来说是一个伤害。 ..我什至不会谈论脚本语言,它们将以相同的静默方式在同一问题上失败)。

但是最后,无论我们要编写多少代码,用户关闭计算机都不会受到保护。

我们已经尽力了,我们已经写了。我自己的偏好是使用throwing finalize方法,未手动完成的非抛出式析构函数清洗资源以及日志/消息框(如果可能)来警告析构函数失败。

也许我们没有进行正确的决斗。而不是" RAII vs. Exception",应该是"试图释放资源与绝对不希望被释放的资源,即使受到破坏威胁时也是如此"

:-)