我应该使用 'std::uncaught_exceptions()' 来决定是否从我的 dtor 抛出异常吗?[已结束]

Should I use `std::uncaught_exceptions()` to decide whether to throw an exception from my dtor? [closed]

提问人:einpoklum 提问时间:11/29/2022 最后编辑:einpoklum 更新时间:11/29/2022 访问量:196

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章来用事实和引文来回答。

12个月前关闭。

我有一个类,其 ctor 进行驱动程序调用,其 dtor 进行匹配的终止/释放驱动程序调用。这些调用可能会失败。问题自然出在dtor上。

我自然知道避免 dtor 中异常的常识,因为如果你在堆栈展开期间抛出一个,你会得到 .但是 - 如果可以的话,我宁愿不要只是“吞下”这些错误而不报告它们。那么,编写代码是否合法/惯用:std::terminate

~MyClass() noexcept(false) {
    auto result = something_which_may_fail_but_wont_throw();
    if (std::uncaught_exceptions() == 0) {
        throw some_exception(result);
    }
}

或者这只是巴洛克风格,不是一个好主意?

注意:此类无权访问标准输出/错误流,也无法访问日志等。

C++ 析构函数 习语 堆栈展开 异常安全

评论

0赞 sideshowbarker 12/2/2022
评论不用于扩展讨论;此对话已移至 Chat

答:

6赞 Brian Bi 11/29/2022 #1

如果您唯一要做的就是检查是否为零,那么您可能会错过一些可以安全传播异常的情况。例如,考虑uncaught_exceptions()

struct X {
    ~X() noexcept(false) {
        if (std::uncaught_exceptions() == 0) throw FooException{};
    }
};

struct Y {
    ~Y() {
        try {
            X x;
        } catch (const FooException&) {
            // handle exception
        }
    }
};

int main() {
    try {
        Y y;
        throw BarException{};
    } catch (const BarException&) {
        // handle 
    }
}

在这里,将在堆栈展开过程中被破坏。在析构函数期间,飞行中有一个未捕获的异常。析构函数创建一个对象,其析构函数随后必须决定是否抛出 .这样做是安全的,因为在它到达将被调用的点之前,将有机会抓住它。但确定未捕获的异常正在运行中,因此它决定不引发异常。yYXFooExceptionFooExceptionstd::terminateX::~X

这在技术上没有任何问题,但 try-catch 块的行为取决于调用的上下文,这可能会令人困惑。理想情况下,在这种情况下仍应引发异常。Y::~YY::~YX::~X

N4152 解释了正确的使用方法:std::uncaught_exceptions

想要知道是否正在运行其析构函数以展开此对象的类型可以在其构造函数中查询并存储结果,然后在其析构函数中再次查询;如果结果不同,则此析构函数将作为堆栈展开的一部分被调用,因为该异常在对象构造之后引发。uncaught_exceptionsuncaught_exceptions

在上面的例子中,需要存储其构造期间的值,即 1。然后,析构函数将看到该值仍为 1,这意味着让异常从析构函数中逃脱是安全的。X::X()std::uncaught_exceptions()

这种技术只应该用于你确实需要从析构函数抛出的情况,并且你没有接受这样一个事实,即如果检查失败,从析构函数抛出的任何目的都将无法实现(迫使析构函数吞下错误条件或终止程序)。这种情况很少见。std::uncaught_exceptions()

评论

0赞 einpoklum 11/30/2022
很公平,但是 - 这并不能完全回答我的问题。我的意思是,是的,我会在比我理论上可能能够做到的更少的情况下抛出异常;我可以添加一个场地来最大限度地发挥投掷的潜力。但无论哪种方式 - 您如何看待这种方法?uncaught_exceptions
0赞 Brian Bi 11/30/2022
@einpoklum我在最后一段中写了一些关于这个问题的想法。
-2赞 lewis 11/29/2022 #2

在任何情况下都不应该从 c++ 析构函数中抛出。

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf

18.5.1 std::terminate() 函数 [except.terminate] ...(1.4) — 当堆栈展开过程中对象被破坏时 (18.2) 通过引发异常而终止,或者

因此,不投入破坏者不仅仅是“智慧”。这将导致您的程序崩溃/退出。

相反,我为这种情况所做的是,有一个类的方法,称为“Complete”。

在“完成”方法中,您可以检查错误代码并安全抛出。

可以添加初始化为 false 的数据成员 (completed) - private - 并在 Complete () 方法中设置 true。在析构函数中,断言其为真(这样您就可以捕获忘记调用 Complete 的任何情况)。

从析构函数中投掷可能会导致程序严重混乱。

评论

2赞 Evg 11/29/2022
这句话在这里不适用。如果在堆栈展开期间调用析构函数,则不会为零,并且不会在 OP 的代码中抛出异常。这就是重点:让投掷成为有条件的,以避免被召唤。std::uncaught_exceptions()std::terminate()
0赞 einpoklum 11/29/2022
1. 我的代码不会导致程序终止。2.方法无济于事,因为我所说的错误只发生在销毁期间,而销毁后,无论如何都无法访问这样的方法。complete()
0赞 lewis 11/29/2022
我不明白为什么你认为你可以在不触发 std::terminate 的情况下从析构函数抛出。也许有一些方法可以避免这种情况。但肯定没有意义。正如我所说,您可以使用完整的方法。这个完整的方法可以执行析构函数会执行的任何操作(包括关闭子对象)。例如,无论您拥有什么直接对象,都可以将它们包装在可选或shared_ptr中,以便在您拥有的对象被销毁之前可以清理它们。或者递归地应用相同的技巧 - 在这些子对象上使用“完成”方法。
1赞 lewis 12/14/2022
@einpoklum - 首先让我道歉。从你的问题中应该很明显。但我一直认为析构函数是 noexcept(true)。我没有意识到它们可能是 noexcept(false),因此没有充分地在脑海中处理您的代码。我的错。不好意思。
1赞 lewis 12/17/2022
@einpoklum 我认为您对您编写的代码的抱怨是您检查 (std::uncaught_exceptions() == 0) 是否可以抛出更多信息;但是您无法处理其他情况(您想对吗 - 并在已经抛出的内容中结合一些新信息?或者你已经很高兴在这种情况下什么都不做,并认为最初抛出的东西有你关心的所有错误信息?如果您不需要合并信息,则一切就绪。