ChatGPT解决这个技术问题 Extra ChatGPT

如果你不应该在析构函数中抛出异常,你如何处理其中的错误?

大多数人说永远不要从析构函数中抛出异常——这样做会导致未定义的行为。 Stroustrup 指出“向量析构函数显式调用每个元素的析构函数。这意味着如果元素析构函数抛出,向量析构失败......确实没有很好的方法来防止析构函数引发的异常,所以库不保证元素析构函数是否抛出”(来自附录 E3.2)。

This article 似乎另有说法 - 抛出析构函数或多或少是可以的。

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

如果在清理操作过程中发生错误,您是否忽略它?如果它是一个可以在堆栈中处理但不能在析构函数中正确处理的错误,那么从析构函数中抛出异常是否有意义?

显然,这类错误很少见,但也有可能。

“一次有两个例外”是一个常见的答案,但这不是真正的原因。真正的原因是当且仅当不能满足函数的后置条件时才应该抛出异常。析构函数的后置条件是对象不再存在。这不可能不发生。因此,在对象超出范围之前,任何容易失败的报废操作都必须作为单独的方法调用(无论如何,明智的函数通常只有一个成功路径)。
@spraff:您是否知道您所说的暗示“扔掉RAII”?
@spraff:必须在对象超出范围之前调用“一个单独的方法”(如您所写)实际上会丢弃 RAII!使用此类对象的代码必须确保在调用析构函数之前调用此类方法。最后,这个想法根本没有帮助。
@Frunsi 不,因为这个问题源于析构函数试图做的事情不仅仅是释放资源。很容易说“我总是想最终做 XYZ”,并认为这是将这种逻辑放入析构函数的论据。不,不要偷懒,写 xyz() 并保持析构函数没有非 RAII 逻辑。
@Frunsi 例如,在表示事务的类的析构函数中,将某些内容提交到文件 不一定 一定可以。如果提交失败,当事务中涉及的所有代码都超出范围时,处理它就太晚了。除非调用 commit() 方法,否则析构函数应丢弃事务。

C
Community

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

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

}

这基本上归结为:

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

然后,析构函数将通过调用这些方法(如果用户没有明确地这样做)来完成对象,但是任何抛出的异常都会被捕获并丢弃(在尝试修复问题之后)。

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

一个例子:

标准::fstream

close() 方法可能会引发异常。如果文件已打开,则析构函数调用 close(),但要确保任何异常都不会传播到析构函数之外。

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

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

编辑:

显然也在“更有效的 C++”
Item 11: Prevent exceptions from leaving destructors


“除非你不介意终止应用程序,否则你应该吞下这个错误。” - 这可能应该是例外(请原谅双关语)而不是规则 - 即快速失败。
我不同意。终止程序会停止堆栈展开。不再调用析构函数。任何打开的资源都将保持打开状态。我认为吞下例外将是首选。
操作系统可以清理它是所有者关闭的资源。内存、文件句柄等。复杂资源呢:数据库连接。您打开的国际空间站的上行链路(它会自动发送关闭的连接)吗?我相信 NASA 会希望你彻底关闭连接!
如果应用程序将通过中止“快速失败”,那么它首先不应该抛出异常。如果通过将控制权传回堆栈而失败,则不应以可能导致程序中止的方式这样做。一个或另一个,不要两个都选。
@LokiAstari 您用于与航天器通信的传输协议无法处理断开的连接?好的...
G
Gal Goldman

抛出析构函数可能会导致崩溃,因为这个析构函数可能被称为“堆栈展开”的一部分。堆栈展开是在抛出异常时发生的过程。在这个过程中,所有从“尝试”到抛出异常之前被压入堆栈的对象都将被终止 -> 它们的析构函数将被调用。并且在这个过程中,不允许再次抛出异常,因为不可能一次处理两个异常,因此,这将引发对 abort() 的调用,程序将崩溃并且控制将返回给操作系统。


您能否详细说明在上述情况下如何调用 abort() 。意味着执行的控制仍由 C++ 编译器控制
@Krishna_Oza:很简单:每当抛出错误时,引发错误的代码都会检查一些位,表明运行时系统正在展开堆栈(即处理其他一些 throw 但没有找到 {2 } 块)在这种情况下调用 std::terminate(不是 abort)而不是引发(新)异常(或继续堆栈展开)。
M
Martin Ba

我们必须在这里区分,而不是盲目地遵循针对特定情况的一般建议。

请注意,以下内容忽略了对象容器的问题以及面对容器内对象的多个 d'tors 该怎么办。 (并且可以部分忽略,因为有些对象不适合放入容器中。)

当我们将类分为两种类型时,整个问题变得更容易思考。一个类 dtor 可以有两种不同的职责:

(R) 释放语义(也就是释放内存)

(C) 提交语义(也就是将文件刷新到磁盘)

如果我们以这种方式看待这个问题,那么我认为可以说(R)语义永远不应该导致来自 dtor 的异常,因为 a)我们对此无能为力,b)许多免费资源操作不会甚至提供错误检查,例如 void free(void* p);

具有 (C) 语义的对象,例如需要成功刷新其数据的文件对象或在 dtor 中执行提交的(“范围保护”)数据库连接属于不同的类型:我们可以对错误做一些事情(在应用程序级别),我们真的不应该像什么都没发生一样继续。

如果我们遵循 RAII 路线并允许在其 d'tors 中具有 (C) 语义的对象,我认为我们还必须允许此类 d'tors 可以抛出的奇怪情况。因此,您不应将此类对象放入容器中,并且如果在另一个异常处于活动状态时 commit-dtor 抛出,程序仍然可以terminate()

关于错误处理(提交/回滚语义)和异常,有一个 Andrei Alexandrescu 的精彩演讲:Error Handling in C++ / Declarative Control Flow(在 NDC 2014 举行)

在详细信息中,他解释了 Folly 库如何为其 ScopeGuard 工具实现 UncaughtExceptionCounter

(我应该注意到 others 也有类似的想法。)

虽然这次演讲的重点不是从 d'tor 扔东西,但它展示了一种今天可以用来摆脱 d'tor 的 problems with when to throw 的工具。

的未来 中, 可能 是一个标准特性, 参见 N3614discussion about it

更新 '17:C++17 标准特性是 std::uncaught_exceptions afaikt。我将快速引用 cppref 文章:

注释 使用int-returning uncaught_exceptions 的一个例子是……首先创建一个保护对象,并在其构造函数中记录未捕获异常的数量。输出由保护对象的析构函数执行,除非 foo() 抛出(在这种情况下,析构函数中未捕获的异常数量大于构造函数观察到的数量)


非常同意。并添加一种语义 (Ro) 回滚语义。常用于范围保护。就像我在项目中定义 ON_SCOPE_EXIT 宏的情况一样。关于回滚语义的情况是,任何有意义的事情都可能在这里发生。所以我们真的不应该忽视失败。
@MartinBa:我认为您错过了我评论的重点,这很令人惊讶,因为我同意您的观点,即 (R) 和 (C) 不同。我想说 dtor 本质上是 (R) 的工具,而 finally 本质上是 (C) 的工具。如果您不明白为什么:考虑一下为什么在 finally 块中抛出异常是合法的,以及为什么对于析构函数not 也是如此。 (从某种意义上说,这是数据与控制的事情。析构函数用于释放数据,finally 用于释放控制。它们是不同的;不幸的是,C++ 将它们联系在一起。)
@Mehrdad:这里太长了。如果你愿意,你可以在这里建立你的论点:programmers.stackexchange.com/questions/304067/…。谢谢。
不同意 (R) 语义的基本原理:“(R) 语义永远不应导致 dtor 异常,因为 a) 我们对此无能为力, b) 许多免费资源操作甚至不提供错误检查。 "关于(b):对于不能失败/不报告错误的免费操作,这个问题不会出现。当这些操作报告错误时,它就会出现。在这种情况下,人们可以做很多事情——但是,当然,不能在析构函数中做,因为它几乎没有上下文。和...
...并且当您需要告诉外部代码存在您自己无法处理的问题时,抛出异常就是您的做法(尤其是当您无法返回错误状态时)。
D
Derek Park

关于从析构函数中抛出的真正问题是“调用者可以用这个做什么?”实际上,您可以对异常做些什么有用的事情,以抵消从析构函数中抛出所产生的危险?

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


刚刚注意到......从 dtor 抛出永远不是未定义的行为。当然,它可能会调用 terminate(),但这是非常明确的行为。
std::ofstream 的析构函数刷新然后关闭文件。刷新时可能会发生磁盘已满错误,您绝对可以做一些有用的事情:向用户显示一个错误对话框,说明磁盘可用空间不足。
首先,日志记录已经足以值得抛出异常(如果不是因为堆栈展开破坏的困难)。记录错误可能非常重要,但被破坏的对象通常不知道如何记录错误。此外,在此类错误之后可能需要/应该做其他事情,例如还释放其他资源或可能重新建立不再发生此类错误的状态(例如网络连接)。所以,底线:调用者可以做很多事情。
@Andy 也是一种常见的策略,即刷新到敏感文件的副本,然后将所述修改后的文件移动到原始文件上。您可能会想像,仅仅因为您继续忽略 fsync 错误,您会如何丢失数据,这会导致您将损坏的文件移动到原始文件上。即使在 fsync 失败的那一刻硬终止程序也比简单地丢失所有内容更安全。但是您可以事先进行备份...如果您不确定 fsync 是否成功,它也会失败。除非你所做的不是很重要,否则你永远不应该忽略这些类型的错误。
l
lothar

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

所以析构函数通常应该捕获异常,而不是让它们传播出析构函数。

为在从 try 块到 throw 表达式的路径上构造的自动对象调用析构函数的过程称为“堆栈展开”。 [注意:如果在堆栈展开期间调用的析构函数因异常退出,则调用 std::terminate (15.5.1)。所以析构函数通常应该捕获异常,而不是让它们传播出析构函数。 ——尾注]


没有回答这个问题 - OP 已经意识到这一点。
@Arafangion 我怀疑他是否意识到这一点(std::terminate 被调用),因为接受的答案完全相同。
@Arafangion 在这里的一些答案中,有些人提到 abort() 被调用;还是 std::terminate 依次调用 abort() 函数。
M
Martin York

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

你要问的是在这种情况下

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

};

另请参阅此FAQ


恢复这个答案,回复:第一个示例,关于 int foo(),如果您愿意,您可以使用函数尝试块将整个函数 foo 包装在 try-catch 块中,包括捕获析构函数。仍然不是首选方法,但它是一回事。
“什么应该捕获异常?应该调用 foo 吗?”是的;或者它可以让它传播。 “为什么 foo 的调用者应该关心 foo 内部的一些对象?” foo 的调用者确实知道内部对象,它会知道 foo() 以某种方式抛出了异常。
“为什么 foo 的调用者应该关心 foo 内部的一些对象?”你是对的,他们不应该,但作为负责任的 C++ 程序员,我认为他们会,他们总是这样做,你可以告诉他们任何你想要的,他们不在乎。海仑定律。 C++ 糟糕的设计可以归咎于此。
G
GaspardP

我所在的小组认为,在析构函数中抛出的“范围保护”模式在许多情况下都很有用 - 特别是对于单元测试。但是,请注意,在 C++11 中,抛出析构函数会导致调用 std::terminate,因为析构函数被隐式注释为 noexcept

Andrzej Krzemieński 有一篇关于抛出的析构函数的精彩帖子:

https://akrzemi1.wordpress.com/2011/09/21/destructors-that-throw/

他指出 C++11 有一种机制可以覆盖析构函数的默认 noexcept

在 C++11 中,析构函数被隐式指定为 noexcept。即使你没有添加规范并像这样定义你的析构函数:class MyType { public: ~MyType() { throw Exception(); } // ... };编译器仍会无形地将规范 noexcept 添加到您的析构函数中。这意味着当你的析构函数抛出异常时,std::terminate 将被调用,即使没有双重异常情况。如果你真的决定允许你的析构函数抛出,你将不得不明确地指定它;你有三个选项:明确指定你的析构函数为 noexcept(false),从另一个已经将其析构函数指定为 noexcept(false) 的类继承你的类。在您的类中放置一个非静态数据成员,该成员已经将其析构函数指定为 noexcept(false)。

最后,如果您决定抛出析构函数,您应该始终注意双重异常的风险(在堆栈因异常而展开时抛出)。这会导致对 std::terminate 的调用,而这很少是您想要的。为了避免这种行为,您可以在使用 std::uncaught_exception() 抛出新异常之前简单地检查是否已经存在异常。


F
Franci Penov

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


T
Tom

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

例如:

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

我正在寻找解决方案,但他们正试图解释发生了什么以及为什么。只是想弄清楚在析构函数内部是否调用了关闭函数?
@JasonLiu 不, close 实际上与析构函数完全分开,几乎没有耦合。这几乎是一个开关,它会导致抛出的某些代码部分过早运行。在析构函数中,您检查它是否已经运行,例如,如果它是一个文件,则跳过关闭它,它已经以某种可预测的状态关闭。但这几乎抛弃了 C++ 对 C... RAII 的唯一真实的东西。你写的代码是原来的两倍。您可以在 fstream 类中看到这一点,如果您不手动关闭文件,析构函数会关闭它并忽略所有错误。
D
DJClayworth

作为良好、全面和准确的主要答案的补充,我想对您引用的文章发表评论——那篇文章说“在析构函数中抛出异常还不错”。

文章采用“抛出异常的替代方案是什么”这一行,并列出了每个替代方案的一些问题。这样做之后,它得出的结论是,因为我们找不到没有问题的替代方案,我们应该继续抛出异常。

问题在于,它列出的备选问题中没有一个问题与异常行为一样糟糕,让我们记住,这是“程序的未定义行为”。作者的一些反对意见包括“审美丑陋”和“鼓励不良作风”。现在你更愿意拥有哪个?一个风格不好的程序,还是一个表现出未定义行为的程序?


不是未定义的行为,而是立即终止。
该标准说“未定义的行为”。这种行为经常被终止,但并非总是如此。
不,请阅读异常处理->特殊功能中的 [except.terminate](在我的标准副本中是 15.5.1,但它的编号可能已经过时)。
@MarcvanLeeuwen 谢谢你的这一点,我发现自己处于一个可以很容易地防止在代码的某些部分中出现双重投掷的地方,如果考虑到不同的用途,我只是将投掷移到析构函数中会更干净无论如何,该特定课程的模式没有意义,但没有意义,因为它是“不好的做法”。希望人们更多地尝试这个概念,因为在某些情况下,由于意外终止而失去 15 分钟的工作感觉比我不知道的要好得多,因为我忘记调用某些函数而导致整个磁盘被破坏。
M
MartinP

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

答:有几种选择:

让异常从你的析构函数中流出,不管其他地方发生了什么。这样做时要注意(甚至害怕) std::terminate 可能会随之而来。永远不要让异常从析构函数中流出。如果可以的话,可能会写入日志,一些大红色的坏文本。我的最爱:如果 std::uncaught_exception 返回 false,则让您的异常流出。如果它返回 true,则回退到日志记录方法。

但是投入d'tors好吗?

我同意上述大部分内容,最好在析构函数中避免投掷,如果可以的话。但有时你最好接受它可能发生,并妥善处理它。我会选择上面的3个。

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


你最喜欢的是我最近尝试过的东西,事实证明你应该这样做。 gotw.ca/gotw/047.htm
我不敢相信实际回答 OP 问题的答案排名如此之低。 +1。
@einpoklum 和其他一切都如此模糊,“你不应该投入析构函数,但我不会提供适当的解决方案”......我不再确定这是货物崇拜还是仅仅是拥有的人不知道试图通过即兴创作来回答这个问题......
佚名

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

主要问题是:你不能不失败。毕竟,失败意味着什么?如果向数据库提交事务失败,并且失败(回滚失败),我们的数据完整性会发生什么?

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

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

务实的解决方案也许只是确保失败失败的可能性在天文数字上是不可能的,因为在某些情况下,让事情不可能失败几乎是不可能的。

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

诚然,这说起来容易做起来难,但这是我看到的唯一真正正确的方法。有时我认为应该有能力为正常的执行路径和异常的执行路径编写单独的析构函数逻辑,因为有时析构函数感觉有点像他们通过尝试处理两者来承担双重责任(一个例子是需要明确解除的范围保护; 如果他们可以区分异常破坏路径和非异常破坏路径,他们就不需要这样做)。

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

最简单的解决方案之一自然是减少使用析构函数。在上面的粒子示例中,也许在破坏/移除粒子时,应该做一些可能由于任何原因而失败的事情。在这种情况下,不是通过可以在特殊路径中执行的粒子的 dtor 调用此类逻辑,而是可以在粒子系统移除粒子时将其全部完成。移除粒子可能总是在非异常路径期间完成。如果系统被破坏,也许它可以只清除所有粒子,而不用打扰可能失败的单个粒子删除逻辑,而可能失败的逻辑仅在粒子系统正常执行期间删除一个或多个粒子时执行。

如果您避免使用非平凡的析构函数处理大量小对象,通常会出现类似的解决方案。你可能会陷入一团乱麻,似乎几乎不可能实现异常安全的地方是,当你确实陷入了许多都具有非平凡 dtors 的小对象中时。

如果任何指定它的东西(包括应该继承其基类的 noexcept 规范的虚函数)试图调用任何可能抛出的东西,如果 nothrow/noexcept 实际转换为编译器错误,这将有很大帮助。这样,如果我们实际上无意中编写了一个可能抛出的析构函数,我们就能够在编译时捕获所有这些东西。


现在破坏就失败了?
我认为他的意思是在失败期间调用析构函数来清理失败。因此,如果在活动异常期间调用析构函数,则它无法从先前的故障中清除。
@user2445507 完全是胡说八道。析构函数是程序的一小部分,他们应该知道的最后一件事是其他析构函数,或者调用它们是因为对象优雅地超出范围还是因为堆栈过早地展开......这就是为什么如果你终止程序在堆栈展开期间抛出,正是因为他们不知道,不应该。
A
Arthur P. Golubev

从析构函数中抛出异常永远不会导致未定义的行为。

将异常抛出析构函数的问题是,成功创建的对象的析构函数在处理未捕获的异常时(在创建异常对象之后直到异常激活的处理程序完成)会被异常处理调用机制;并且,如果在处理未捕获的异常时调用的析构函数中的此类附加异常中断了处理未捕获的异常,它将导致调用 std::terminate(调用 std::exception 时的另一种情况是任何处理程序都没有处理异常,但是这个与任何其他函数一样,无论它是否是析构函数)。

如果正在处理未捕获的异常,您的代码永远不知道是否会捕获额外的异常或将归档未捕获的异常处理机制,因此永远无法确定抛出是否安全。

虽然,有可能知道正在处理未捕获的异常(https://en.cppreference.com/w/cpp/error/uncaught_exception),因此您可以通过检查条件来过度杀伤,并且仅在不是这种情况时才抛出(在某些情况下它不会抛出会很安全)。

但在实践中,这种分离为两种可能的行为是没有用的——它只是无助于你制作一个设计良好的程序。

如果您忽略未捕获的异常处理是否正在进行而抛出析构函数,为了避免可能调用 std::terminate,您必须保证在对象的生命周期中抛出的所有异常都可能从其析构函数中抛出异常被捕获在开始销毁对象之前。它的使用非常有限;您几乎不能使用所有可以合理地以这种方式从析构函数中抛出的类;并且仅对某些类的此类使用受限的类允许此类异常的组合也阻碍了设计良好的程序。


M
MRN

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


D
Devesh Agrawal

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

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

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


将消息写入日志文件可能会导致异常。
@Konard 并投入析构函数不能......我仍在寻找一个真正的解释,为什么每个人都如此反对这一点,因为到目前为止,在我考虑这样做的每个场景中,这对我来说实际上是有意义的。
@Sahsahae 我认为如果您使用 try and catch around log operation in destructor 应该没问题。
@Konard 但问题是,如果它抛出你不能记录它或做任何事情,它是无用的,就像记录(然后只是忽略错误)本身一样。异常是异常的,但控制流仍然存在,忽略它只会导致错误,即使您阅读日志并注意到问题,其中一些也无法恢复,例如损坏的文件或数据库......
M
Matthew

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

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

关键是容器保持中立,由所包含的类决定它们在从其析构函数中抛出异常时是否行为不当。


u
user3726672

Martin Ba(上图)走在正确的轨道上——你为 RELEASE 和 COMMIT 逻辑构建不同的架构。

对于发布:

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

对于提交:

这是您需要与 std::lock_guard 等为互斥锁提供的相同类型的 RAII 包装器对象的地方。有了这些,您根本不会将提交逻辑放在 dtor 中。你有一个专用的 API,然后包装对象将 RAII 提交到他们的 dtors 并在那里处理错误。请记住,您可以很好地在析构函数中捕获异常;发行它们是致命的。这还允许您通过构建不同的包装器(例如 std::unique_lock 与 std::lock_guard )来实现策略和不同的错误处理,并确保您不会忘记调用提交逻辑——这是唯一的中途将它放在第一名的正当理由。