C++中非虚析构函数异常问题

14

当我们走出catch块的作用域时,异常析构函数会被调用吗? (假设我们没有重新抛出它)

假设我有一个类A,它的析构函数不是虚拟的。 B继承A。 假设某个函数抛出了B类对象作为异常, 并且它被catch块捕获。

catch(A& a){
...
}

如果异常析构函数应该在离开catch作用域时被调用,在这种情况下,只有基类A的析构函数将被调用吗?

Cornstalks:实际试验结果表明两个类的析构函数都会被调用。

这与我的逻辑相矛盾。有人能解释一下吗?


1
我有点对你为什么问这个问题感兴趣;这是一个非常合理,不算基础的问题,但它表明你关心异常析构函数被调用的时间点,这通常不是你需要关注的。 - Marcus Müller
哎呀,我不确定最后一部分。 - Lightness Races in Orbit
@MarcusMüller:想要了解我们使用的工具有什么问题吗? - Lightness Races in Orbit
2
@LightnessRacesinOrbit:什么都没有!完全没有!这是一个非常好的问题。但是,在异常对象的生命周期内花费时间确实很不寻常;当然,您不希望异常混乱您的内存 :) - Marcus Müller
4个回答

5
当我们走出catch块的范围时,即使我们没有重新抛出异常,在这种情况下,异常析构函数也会被调用吗?
是的:
[C++11: 15.1/4]: [...] 异常对象在最后一个剩余的活动异常处理程序通过任何除重新抛出之外的方式退出或者最后一个引用异常对象的std::exception_ptr类型的对象被销毁后被销毁,以较晚者为准。[...]

如果在 catch 块结束时需要调用异常析构函数,在这种情况下,只会调用基类 A 的析构函数吗?

不是这样的。

#include <iostream>

struct A
{
    A() { std::cout << "A()"; }
    A(const A&) { std::cout << "A(const A&)"; }
    A(A&&) { std::cout << "A(A&&)"; }
    ~A() { std::cout << "~A()"; }
};

struct B : A
{
    B() { std::cout << "B()"; }
    B(const B&) { std::cout << "B(const B&)"; }
    B(B&&) { std::cout << "B(B&&)"; }
    ~B() { std::cout << "~B()"; }
};

int main()
{
    try {
        throw B();
    }
    catch (A&) {
    }
}

// Output: A()B()~B()~A()

实际上,你的示例表明情况并非如此。我使用相同的在线编译器得到以下输出:B()A()。 - zdan
1
啊,Lightness,你的coliru链接显示了~B()~A()都被打印出来了...这正是Cornstalks的答案所确认的...只是说一下。 - Super-intelligent Shade
@InnocentBystander:我猜无法确定~B()是_throw-expression_中的临时对象,还是被省略(合法),我们看到的是来自catch的输出。 - Lightness Races in Orbit
@InnocentBystander:好的,已经修复了 - 现在是真正的证明。 - Lightness Races in Orbit

5

好的,有人已经回答了你的第一个问题。我会关注这个问题:

如果在catch范围之外调用异常析构函数,那么只会调用基类A的析构函数吗?

无论异常是如何被捕获的,实现都将正确销毁异常对象。实现构造异常对象,因此它知道如何销毁它。与通过指针调用delete不同,在那种情况下,在该点上关于对象的完整类型存在不完全的信息(它可能已经在其他地方进行了new),除非存在虚拟析构函数。

如果不是这样,catch (...)将根本无法工作。


(它可能已经在其他地方被newed了)- 你能解释一下吗? - Day_Dreamer
3
为了完整起见,当基类型没有虚析构函数时,通过指向基类型的指针删除指向派生对象的指针会产生未定义的行为。这可能会运行基类的析构函数,但也可能会执行完全不同的操作。 - Pete Becker
@Day_Dreamer 抱歉,那句话有点尴尬,但重点是在代码中指针被delete的时候,没有办法将其与同一对象被new的地方匹配起来。这就是为什么需要虚析构函数,否则代码无法“知道”要调用哪个析构函数。 - Brian Bi
你的解释不够令人信服。就像delete可能与new位置相距甚远一样,catch也可能与throw位置相距甚远。需要一个示例和/或标准引用来支持解释。 - Lightness Races in Orbit
throw 将执行转移到某个蹦床,然后跳到 catch 块中。从 catch 块退出时,会有另一个蹦床销毁异常对象(如果没有 exception_ptr 对象引用它)。这些块成对生成,并可以在静态上匹配。至少这是我理解的;我可能对此有所误解。 - Brian Bi
显示剩余2条评论

3

虽然我没有引用标准,但似乎抛出B并捕获A&会导致AB的析构函数都被调用。 演示链接:

#include <iostream>

struct A
{
    ~A() { std::cout << "A::~A" << std::endl; }
};

struct B : public A
{
    ~B() { std::cout << "B::~B" << std::endl; }
};

void throwit()
{
    throw B{};
}

int main()
{
    std::cout << "beginning main scope" << std::endl;

    {
        std::cout << "beginning inner scope" << std::endl;

        try
        {
            std::cout << "calling throwit()" << std::endl;
            throwit();
        }
        catch (A& a)
        {
            std::cout << "caught exception" << std::endl;
        }

        std::cout << "ending inner scope" << std::endl;
    }

    std::cout << "ending main scope" << std::endl;
}

输出结果:

进入主范围
进入内部范围
调用throwit()
捕获异常
B::~B
A::~A
结束内部范围
结束主范围

正如您所看到的,两个析构函数都被调用了。额外的范围打印清楚地显示了析构函数何时被调用(在 catch块的末尾)。


3
无论何时标准说一个对象被销毁,它都意味着正确的最终派生类析构函数被调用。
总是这样。
当你多态地删除一个没有虚析构函数的对象,或者终止一个不完整类型的对象(通过delete操作符或显式析构函数调用),而适当的析构函数是非平凡的,则标准并不表示该对象已被销毁。它不表示基类的析构函数被调用。它表示你有未定义的行为。

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