在MSVC中的析构函数中抛出异常的异常

时间:2013-10-21 19:37:48

标签: c++ visual-c++ exception-handling 64-bit destructor

我一直在使用C ++中的异常处理和析构函数异常来更好地理解它,并且我遇到了一些我无法解释的奇怪行为(我希望有人可以帮助我)。

我写过这个简单的代码。 这是一个异常类(Foo),它在被破坏时抛出。 这里的目的是让一个异常从它被抛出到main()的任何地方传播,在那里我明确地捕获它并阻止它重新抛出自己。

#include <iostream>

class Foo
{
public:
    Foo();

    virtual ~Foo();

    void stopThrowing() { keepThrowing_ = false; }

private:
    bool keepThrowing_;
};

Foo::Foo(): keepThrowing_(true)
{
    std::cout << "Foo created: " << this << std::endl;
}

Foo::~Foo()
{
    std::cout << "Foo destroyed: " << this << std::endl;

    if (keepThrowing_)
    {
        throw Foo();
    }
}

int main()
{
    try {
        try {
            throw Foo();
        } catch (const Foo&) {
            std::cout << "Foo caught" << std::endl;
        }
    } catch (Foo& ex) {
        std::cout << "Foo caught 2" << std::endl;
        ex.stopThrowing();
    } catch (...) {
        std::cout << "Unknown exception caught 2" << std::endl;
    }

    std::cout << "Done" << std::endl;

    return 0;
}

我知道这绝不应该在C ++中完成,但这不是重点 - 我正在试图理解MSVC中x86和x64异常处理之间的不同之处(我将在下一段中解释)。

当我使用MSVC为x86编译此代码时(我主要使用2010年,但我在2005年和2012年也检查了这一点),一切都很好,并且按预期工作:

Foo created: 001AFC1C
Foo caught
Foo destroyed: 001AFC1C
Foo created: 001AF31C
Foo caught 2
Foo destroyed: 001AF31C
Done

当我使用MSVC为x64编译此代码时,它会失败:

Foo created: 000000000047F9B8
Foo caught
Foo destroyed: 000000000047F9B8
Foo created: 000000000047D310
Foo destroyed: 000000000047D310
Foo created: 000000000047C150
Foo destroyed: 000000000047C150
Foo created: 000000000047AF90
Foo destroyed: 000000000047AF90
Foo created: 0000000000479DD0
...

此时,它一直在创建和销毁Foo对象,直到它到达堆栈溢出并崩溃。

如果我将析构函数更改为此片段(抛出int而不是Foo):

Foo::~Foo()
{
    std::cout << "Foo destroyed: " << this << std::endl;

    if (keepThrowing_)
    {
        throw 1;
    }
}

我收到以下输出:

Foo created: 00000000008EF858
Foo caught
Foo destroyed: 00000000008EF858

然后程序在执行throw 1;时达到调试断言(调用std :: terminate())。

我的问题是:这里发生了什么? 看起来x64上的MSVC不支持这种行为,但它感觉不对,因为它确实可以在x86上运行。我使用MinGW和MinGW-w64为x86和x64编译了这段代码,两个程序都按预期工作。这是MSVC中的错误吗? 任何人都可以想办法绕过这个问题,或者为什么微软决定阻止这种情况发生在x64上?

感谢。

1 个答案:

答案 0 :(得分:2)

我认为32位和64位不同的原因是32位版本使用copy elision而64位版本不使用{{3}}。我可以使用gcc标志在-fno-elide-constructors中重现64位版本的结果。

64位版本中发生的是任何throw Foo();行创建一个临时Foo对象,然后将其复制到存储异常值的任何位置。然后销毁临时Foo,这将导致执行另一个throw Foo();行,这将创建另一个被复制和销毁的临时行,依此类推。如果添加带有print语句的复制构造函数,您应该看到它在64位版本中重复调用,而在32位版本中根本不会调用它。


至于你的throw 1版本调用std::terminate的原因,那是因为如果在析构函数中抛出异常而另一个异常仍然被传播,则std::terminate被调用,因为没有办法一次处理两个例外。所以你首先在main throw Foo(),然后当临时Foo被销毁时,它会在其析构函数中抛出1,但是Foo异常已经被处理,所以程序只是放弃并抛出std::terminate

现在您可能想知道为什么在使用throw Foo();时不会发生这种情况。因为没有复制省略,Foo::~Foo()中实际上不会抛出异常。您在main中的第一次throw Foo()调用会创建一个临时的Foo,然后将其复制,然后调用其析构函数(尚未抛出该副本)。在该析构函数中,另一个临时Foo对象被创建,复制,然后被销毁,这将创建另一个临时Foo ...等等。所以程序一直在制作副本,直到它崩溃,并且实际上从未达到它抛出那些Foo异常的程度。