大多数人说 从来没有 扔一个例外的一个析构-这样做的结果未定义的行为。Stroustrup指出, "矢量析构明确地援引析构为每一个元素。这意味着,如果一个元素析构抛出,矢量的毁灭失败...真的是没有好的方式来保护针对的例外抛出析构,所以图书馆没有保障,如果一个元素析构引发"(从附录E3。2).

这篇文章 似乎在说否则扔析构或多或少是好的。

所以我的问题是这样的-如果投掷从析构,结果不确定的行为,你怎么处理过程中出现的错误一析构?

如果发生错误期间的清理操作,你只是忽视它吗?如果它是错误的可能处理的堆但不析构,它没有道理扔一个例外的destructor?

显然,这些类型的错误是罕见的,但是可能的。

有帮助吗?

解决方案

从析构函数中抛出异常是危险的。
如果另一个异常已经传播,则应用程序将终止。

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

这基本归结为:

任何危险的东西(即可能抛出异常)都应该通过公共方法(不一定是直接的)来完成。然后,您的类的用户可以通过使用公共方法并捕获任何潜在的异常来处理这些情况。

然后析构函数将通过调用这些方法来完成对象(如果用户没有明确地这样做),但是捕获和删除任何异常throw(在尝试修复问题之后)。

因此,您实际上将责任传递给用户。如果用户能够纠正异常,他们将手动调用相应的函数并处理任何错误。如果对象的用户不担心(因为对象将被销毁),那么析构函数将留下来处理业务。

一个例子:

的std :: fstream的

close()方法可能会抛出异常。 如果文件已被打开,析构函数会调用close(),但要确保任何异常都不会从析构函数中传播出来。

因此,如果文件对象的用户想要对与关闭文件相关的问题进行特殊处理,他们将手动调用close()并处理任何异常。另一方面,如果他们不关心那么析构函数将留给处理这种情况。

Scott Myers在他的书“Effective C ++”中有一篇关于这个主题的优秀文章。

编辑:

显然也在“更有效的C ++”中 第11项:防止异常离开析构函数

其他提示

抛出析构函数会导致崩溃,因为这个析构函数可能会被称为“Stack unwinding”的一部分。 堆栈展开是在抛出异常时发生的过程。 在此过程中,自“尝试”以来被推入堆栈的所有对象都被置于堆栈中。并且直到异常被抛出,将被终止 - &gt;他们的毁灭者将被召唤。 并且在此过程中,不允许另一个异常抛出,因为一次不能处理两个异常,因此,这将引发对abort()的调用,程序将崩溃并且控制将返回到OS。

我们必须 区分 在这里,而不是盲目地下 一般 建议 具体 情况。

注意下面 忽略了 这一问题的容器的对象,并在面临的多个d监测的对象,内容器。(它可以忽略部分,因为一些对象只是没有好适合放入容器。)

整个问题变得更易于认为当我们分类两种类型。一个类dtor可以有两种不同的职责:

  • (R)释放的语(又名的免费存储器)
  • (C) 提交 语义(aka 冲水 文件的磁盘)

如果我们查看的问题,这种方式,那么我认为它可以被认为(R)语义不应该造成一种异常dtor有一)我们可以做什么和b)许多自由资源行动甚至不提供错误的检查,例如 void free(void* p);.

对象与(C)语义,如文件目的,需要成功冲洗的数据或("范围内看守")的数据库连接,并提交在dtor是一个不同的类型:我们 可以 做些什么的错误(在应用程序级别上)以及我们真的不应该继续,因为如果什么都没有发生。

如果我们遵循的RAII的路线,并允许的对象(C)语在他们d监测,我认为我们随后还必须允许为奇怪的情况下这样d监测可以扔掉。这下,你不应该把这样的对象进入集装箱和它也遵循这一程序仍可以 terminate() 如果提交dtor引发的同时,另一个异常活跃。


关于错误的处理(承诺/Rollback义)和例外情况,有一个很好的交谈,通过一个 安德烈*Alexandrescu: 中的错误处理C++/声明的控制流程 (举行 NDC2014年)

在详细信息时,他解释了多么愚蠢的图书馆实施了一个 UncaughtExceptionCounter 他们 ScopeGuard 工具。

(我应该注意, 其他人 也有类似的想法。)

虽然谈不上扔从d'tor,它显示了一个工具,可以用 今天 摆脱 问题的时候扔 从一个d'tor.

未来, 有 被一个标准功能,用于这一点, 看看 N3614, 和一个 讨论有关它.

Upd'17:C++17std特征是 std::uncaught_exceptions afaikt.我很快就会报cppref文章:

注意到

一个例子在哪里 int-返回 uncaught_exceptions 用。...第一 创建一个保护对象和记录的数量未捕获的例外 在其构造。输出为执行通过的防护对象的 析构,除非foo()throws(在这种情况下的数量的未捕获的 例外情况析构大于什么构造 观察到的)

要问自己关于从析构函数中抛出的真正问题是“调用者可以对此做什么?”实际上是否有任何有用的异常,可以抵消从析构函数中抛出的危险?

如果我销毁 Foo 对象,并且 Foo 析构函数抛出异常,我可以合理地做些什么呢?我可以记录它,或者我可以忽略它。就这样。我无法“修复”它,因为 Foo 对象已经消失了。最好的情况,我记录异常并继续,好像什么也没发生(或终止程序)。这是否真的值得通过从析构函数中抛出来导致未定义的行为?

它很危险,但从可读性/代码可理解性的角度来看也没有意义。

在这种情况下你要问的是什么

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

什么应该抓住异常?应该是foo的来电者吗?或者应该foo处理它? foo的调用者为什么要关心foo内部的某个对象?可能有一种方法可以让语言将其定义为有意义,但它会变得难以理解且难以理解。

更重要的是,Object的内存在哪里?对象所拥有的内存在哪里?它仍然被分配(表面上是因为析构函数失败了)?还要考虑对象是在堆栈空间中,所以无论如何它都明显消失了。

然后考虑这个案例

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

当删除obj3失败时,如何以保证不失败的方式实际删除?我的记忆是该死的!

现在考虑在第一个代码片段中,Object会自动消失,因为它在堆栈上而Object3在堆上。由于指向Object3的指针消失了,你就是SOL了。你有内存泄漏。

现在一种安全的做事方式如下

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

另见常见问题解答

来自C ++的ISO草案(ISO / IEC JTC 1 / SC 22 N 4411)

因此,析构函数通常应该捕获异常,而不是让它们从析构函数中传播出来。

  

3为从try块到throw的路径构造的自动对象调用析构函数的过程 -     表达式被称为&#8220;堆栈展开。&#8221; [注意:如果在堆栈展开期间调用的析构函数以退出方式退出     异常,调用std :: terminate(15.5.1)。所以析构函数通常应该捕获异常而不是让它们     它们从析构函数中传播出来。 &#8212;结束说明]

您的析构函数可能正在其他析构函数链中执行。抛出未被直接调用者捕获的异常会使多个对象处于不一致状态,从而导致更多问题,然后忽略清理操作中的错误。

其他人都解释了为什么抛出破坏者是可怕的......你能做些什么呢?如果您正在执行可能失败的操作,请创建一个单独的公共方法来执行清理并可以抛出任意异常。在大多数情况下,用户会忽略它。如果用户想要监视清理的成功/失败,他们可以简单地调用显式清理例程。

例如:

class TempFile {
public:
    TempFile(); // throws if the file couldn't be created
    ~TempFile() throw(); // does nothing if close() was already called; never throws
    void close(); // throws if the file couldn't be deleted (e.g. file is open by another process)
    // the rest of the class omitted...
};

作为主要答案的补充,这些答案是好的,全面的和准确的,我想对你引用的文章发表评论 - 那个说“在析构函数中抛出异常并不是那么糟糕”的文章。

该文章采用了“抛出异常的替代方法”这一行,并列出了每种替代方案的一些问题。这样做就得出结论,因为我们找不到无问题的替代方案,所以我们应该继续抛出异常。

问题在于,它与替代品列出的任何问题都不会像异常行为那样糟糕,我们记得,这是“程序未定义的行为”。一些作者的反对意见包括“美学丑陋”。和“鼓励不好的风格”。那你想要哪个?一个风格不好的程序,或者表现出不确定行为的程序?

我在组认为这"范围内的保护"的模式扔在destructor是有用的,在许多情况下,特别是为单元的测试。但是,要知道,在C++11,扔在一析构的结果,在一个呼叫 std::terminate 由于析构函数是隐含地注解 noexcept.

安杰伊*Krzemieński有一个伟大的职位上的专题析构,抛出:

他指出,C++11有一个机制,以复盖默认的 noexcept 为析构:

C++11、析构是隐含地规定为 noexcept.甚至如果添加没有规范和定义析构这样的:

  class MyType {
        public: ~MyType() { throw Exception(); }            // ...
  };

编译器将仍然不可见的方式加说明书 noexcept 你析构函数。这意味着的那一刻你的destructor引发一个例外, std::terminate 会被称为,即使没有双例外情况。如果你真的确定允许您析构扔,你将指定这种明确;你有三个选项:

  • 明确指定你为析构 noexcept(false),
  • 继承你的班级,从另一个已经指定其析构作 noexcept(false).
  • 把一个非静态数据成员类中已经指定其析构作 noexcept(false).

最后,如果你决定要扔在这析构,应始终意识到的风险的一个双例外(扔同时堆叠正放松因为一个例外)。这会导致一个呼叫 std::terminate 这是很少你想要什么。为了避免这种行为,你可以简单地检查,如果已经有一个例外之前扔一个新的使用 std::uncaught_exception().

  问:所以我的问题是 - 如果   从析构函数中抛出导致   未定义的行为,你如何处理   析构函数期间发生的错误?

答:有几种选择:

  1. 让异常从析构函数中流出,无论其他地方发生了什么。在这样做时,请注意(甚至可怕)std :: terminate可能会跟随。

  2. 永远不要让异常从析构函数中流出。如果可以,可以写入日志,一些大的红色坏文本。

  3. my fave :如果 std :: uncaught_exception 返回false,则让异常流出。如果它返回true,则返回到日志记录方法。

  4. 但扔进去是不是很好?

    我同意上面的大部分内容,在析构函数中最好避免抛出它。但有时你最好接受它可以发生,并妥善处理它。我选择3以上。

    有一些奇怪的情况,它实际上是一个好主意从析构函数中抛出。 就像“必须检查”一样错误代码。这是从函数返回的值类型。如果调用者读取/检查包含的错误代码,则返回的值将以静默方式销毁。 但是,如果在返回值超出范围时尚未读取返回的错误代码,则会从其析构函数中抛出一些异常

我目前遵循这个政策(很多人都说),类不应该主动从他们的析构函数中抛出异常,而应该提供一个公共的“关闭”。执行可能失败的操作的方法......

...但我确实认为容器类类的析构函数(如向量)不应该掩盖从它们包含的类中抛出的异常。在这种情况下,我实际上使用了“免费/关闭”。递归调用自身的方法。是的,我递归地说。这种疯狂有一种方法。异常传播依赖于存在堆栈:如果发生单个异常,则剩余的析构函数仍将运行,并且一旦例程返回,挂起的异常将传播,这很好。如果发生多个异常,那么(取决于编译器)要么传播第一个异常,要么终止程序,这没关系。如果有这么多异常发生,递归溢出堆栈然后出现严重错误,有人会发现它,这也没关系。就个人而言,我犯错误的一面,而不是隐藏,秘密和阴险。

重点是容器保持中立,并且由所包含的类来决定它们是否在从析构函数中抛出异常时表现或行为异常。

Martin Ba(上图)走在正确的轨道上 - 您对RELEASE和COMMIT逻辑的架构不同。

For Release:

你应该吃任何错误。你释放内存,关闭连接等等。系统中的其他任何人都不应该再次看到这些东西,而是将资源交回操作系统。如果您在此处需要真正的错误处理,则可能是对象模型中设计缺陷的结果。

For Commit:

这就是你想要的那种RAII包装器对象,像std :: lock_guard这样的东西正在为互斥体提供。有了那些你没有把提交逻辑放在dtor AT ALL中。你有一个专用的API,然后包装对象,RAII会在他们的dtors中提交它并处理那里的错误。记住,你可以在析构函数中捕获异常就好了;它发布它们是致命的。这也允许您通过构建不同的包装器(例如std :: unique_lock与std :: lock_guard)来实现策略和不同的错误处理,并确保您不会忘记调用提交逻辑 - 这是唯一的中途将它放在第一位的dtor中的合理理由。

设置闹钟事件。通常,警报事件是在清理对象时通知故障的更好形式

与构造函数不同,抛出异常可以是指示对象创建成功的有用方法,不应在析构函数中抛出异常。

在堆栈展开过程中从析构函数抛出异常时会发生此问题。如果发生这种情况,则编译器处于不知道是继续堆栈展开过程还是处理新异常的情况。最终结果是您的程序将立即终止。

因此,最好的做法是完全避免在析构函数中使用异常。请将消息写入日志文件。

  

所以我的问题是 - 如果从析构函数中抛出结果   未定义的行为,你如何处理在a。期间发生的错误   析构?

主要问题是:你不能失败。毕竟,未能失败意味着什么?如果将事务提交到数据库失败,并且它无法失败(无法回滚),那么数据完整性会发生什么变化?

由于正常路径和异常(失败)路径都会调用析构函数,因此它们本身不会失败,否则我们就会“失败”。

这是一个概念上难以解决的问题但通常解决方案是找到一种方法来确保失败不会失败。例如,数据库可能会在提交到外部数据结构或文件之前写入更改。如果事务失败,则可以丢弃文件/数据结构。所有它必须确保从外部结构/文件提交一个不会失败的原子事务的更改。

  

务实的解决方案或许只是确保机会   失败的失败在天文学上是不可能的,因为制造东西   在某些情况下,不可能失败是不可能的。

对我来说最合适的解决方案是以一种清理逻辑不会失败的方式编写非清理逻辑。例如,如果您想要创建新的数据结构以清理现有的数据结构,那么您可能会提前创建该辅助结构,以便我们不再需要在析构函数中创建它。 / p>

不可否认,这说起来容易做起来难得多,但这是我认为唯一正确的方法。有时候我认为应该有能力为正常的执行路径编写单独的析构函数逻辑而不是特殊的执行路径,因为有时析构函数感觉有点像他们通过尝试处理两者而承担双重责任(例如需要明确解雇的范围保护程序) ;如果他们能够将特殊的破坏路径与非特殊破坏路径区分开来,他们就不会要求这样做。)

最终的问题仍然是我们不能失败,在所有情况下完全解决都是一个难以理解的概念设计问题。如果你没有太多包裹在复杂的控制结构中,并且大量的小物体彼此相互作用,而是以稍微笨重的方式模拟你的设计(例如:带有析构函数的粒子系统来破坏整个粒子),它会变得更容易系统,而不是每个粒子单独的非平凡析构函数)。当您在这种较粗糙的级别上对您的设计进行建模时,您可以处理更少的非平凡的析构函数,并且通常还可以承担所需的内存/处理开销,以确保您的析构函数不会失败。

这是最简单的解决方案之一,自然就是不经常使用析构函数。在上面的粒子示例中,也许在破坏/移除粒子时,应该做一些可能由于某种原因而失败的事情。在这种情况下,不是通过可以在异常路径中执行的粒子的dtor调用这样的逻辑,而是可以在粒子系统移除粒子时完成所有操作。在非特殊路径中,可能始终会移除粒子。如果系统被破坏,它可能只是清除所有粒子,而不是打扰可能失败的单个粒子移除逻辑,而可以失败的逻辑只在粒子系统正常执行时执行,当它移除一个或多个粒子时。 / p>

如果你避免使用非平凡的析构函数来处理很多很少的对象,那么通常会出现这样的解决方案。你可以在乱七八糟的地方纠结在一起似乎几乎不可能除外

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top