在析构函数内处理异常(但不抛出)

3
我知道,如果在析构函数中抛出异常,程序会在堆栈展开期间中止执行,因为那时将传播超过1个异常。
这里有一个带注释的示例来演示这一点:
class Foo
{
public:
    ~Foo()
    {
        ReleaseResources();
    }

private:
    int* pInt;

    void ReleaseResources()
    {
        if (!pInt)
            throw 0;
        else delete pInt;
    }
};

int main() try
{
    {
        Foo local;
        throw 1;
    } // aborting here, because now 2 exceptions are propagating!

    return 0;
}
catch (int& ex)
{
    return ex;
}

然而,我有一个类层次结构,在其中一个析构函数调用可能引发异常的函数时,由于入口层次结构被污染,现在所有析构函数都标记为noexcept(false)
尽管编译器可以插入异常代码,但对于这些类的用户来说,这并不可行,因为它不能阻止程序在上述代码样例中发生异常而导致中止。
因为我希望析构函数是异常安全的,所以我想到将它们全部标记为noexcept,但要像这样处理可能出现的异常:
同样的示例,但重新设计以避免中止情况,并使析构函数具备异常安全性:
class Foo
{
public:
    ~Foo() noexcept
    {
        try
        {
            ReleaseResources();
        }
        catch (int&)
        {
            // handle exception here
            return;
        }
    }

private:
    int* pInt;

    void ReleaseResources()
    {
        if (!pInt)
            throw 0;
        else delete pInt;
    }
};

int main() try
{
    {
        Foo local;
        throw 1;
    } // OK, not aborting here...

    return 0;
}
catch (int& ex)
{
    return ex;
}

问题是,处理析构函数内部异常的这种常规方法是否正常?是否存在可能导致此设计失败的示例?

主要目标是具有异常安全的析构函数。

另外一个问题是,在第二个示例中,在堆栈展开期间仍然有2个异常传播,为什么没有调用abort?如果只允许在堆栈展开期间出现一个异常?

2个回答

1
问题是,在析构函数中处理异常的这种方法是否正常?有哪些例子可能会导致这种设计出现问题?
如果您的“//在此处处理异常”代码实际上处理异常,那么您可以避免像这样抛出析构函数。但是,在实践中,如果您在析构期间抛出异常,通常意味着没有好的方法来处理异常。
从析构函数中抛出异常意味着某些清理操作失败了。也许资源泄漏,数据无法保存并且现在已经丢失,或者某些内部状态无法设置或还原。无论原因如何,如果您可以避免或修复问题,您就不必首先抛出异常。
您对这种糟糕情况(抛出析构函数)的解决方案仅在您实际上不处于糟糕的情况下才有效。在实践中,如果您尝试应用此方法,您会发现除了警告用户或记录问题之外,没有任何可以编写“//在此处处理异常”的内容。
如果在堆栈展开期间只允许一个异常吗?
没有这样的规则。在堆栈展开期间抛出异常的问题在于,如果未捕获的异常从析构函数中逃逸出来。如果析构函数引发并在内部捕获异常,则对正在进行的堆栈展开没有影响。std::terminate明确说明当堆栈展开以终止结束时(link):

在某些情况下,必须放弃异常处理,转而采用更不太微妙的错误处理技术。这些情况包括:

[...]

-- 在堆栈展开期间对象的销毁因引发异常而终止,或

[...]


我应该提到,在析构函数中调用可能会抛出异常的函数是来自第三方库的函数,例如COM中的Release()。我无法控制该函数,我只知道它可能会抛出异常。因此,如果程序在不应该干净地关闭但需要继续运行时,如果该函数抛出异常,则可能记录问题并手动中止是解决问题的方法? - metablaster
@metablaster 记录和终止可能是您合理执行的唯一操作。进行干净的关闭可能是可以的,但是取决于为什么首先出现异常,这也可能不成功。例如,如果关闭取决于该库的其他功能,则它们也可能无法正常工作。 - François Andrieux

1
~Foo() noexcept
在这种情况下,noexcept是多余的,因为没有具有潜在抛出析构函数的子对象。即使没有noexcept,析构函数也会被隐式视为noexcept

问题是,这是处理析构函数内异常的正常方法吗?

通常使用try-catch来处理异常,无论是在析构函数内部还是其他地方。
然而,在这种特殊情况下,更好的解决方案是:
void ReleaseResources()
{
    delete pInt;
}

不需要在这里抛出异常,不抛出异常会更简单。

还有一个问题,在第二个例子中,在堆栈展开期间仍然有2个异常正在传播,为什么没有调用abort()?

因为是允许的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接