C++:通过引用引发派生类在捕获基类时不起作用

C++: Throwing a derived class by reference does not work when catching base class

本文关键字:基类 不起作用 派生 引用 C++      更新时间:2023-10-16

我想用基类Exception抛出我自己的异常。有一个虚拟方法print,它将被子类覆盖。我只捕获类型Exception&,并使用print来获得特定的错误。问题是,一旦我抛出一个子类的引用,它就会被当作基类来处理。

这里有一个例子:

#include <iostream>
using namespace std;
class Exception
{
    public:
        virtual void print()
        {
            cout << "Exception" << endl;
        }
};
class IllegalArgumentException : public Exception
{
    public:
        virtual void print()
        {
            cout << "IllegalArgumentException" << endl;
        }
};
int main(int argc, char **argv)
{
    try
    {
        IllegalArgumentException i;
        Exception& ref = i;
        cout << "ref.print: ";
        ref.print();
        throw ref;
    }
    catch(Exception& e)
    {
        cout << "catched: ";
        e.print();
    }
}

这个例子的输出是:

ref.print: IllegalArgumentException
catched: Exception

使用引用应该会导致使用派生类的print方法。在try块中,引用确实使用了它。为什么捕获的Exception&的行为不像IllegalArgumentException,我如何获得这种行为?

下面的代码似乎做了它应该做的事情:

try
{
    IllegalArgumentException i;
    Exception* pointer = &i;
    throw pointer;
}
catch(Exception* e)
{
    cout << "pointer catched: ";
    e->print();
}

但是指针在try块的范围之外是否可能变得无效?这样做是有风险的,如果我在堆上分配内存来解决这个问题,我就有责任删除catch块中的内容,这也不太好。那么你将如何解决这个问题呢?

throw隐式复制,从而进行切片。引用C++11,§15.1/3:

throw表达式初始化一个临时对象,称为异常对象,其类型是通过从throw操作数的静态类型中删除任何顶级cv限定符并将类型从"array of T"或"function returning T"调整为"pointer to T"或"pointer for function returned T"来确定的,分别地临时是一个左值,用于初始化匹配的处理程序中命名的变量。如果异常对象的类型是一个不完整的类型或指向除(可能是cv限定的)void之外的不完整类型的指针,则程序是格式错误的。除了这些限制和15.3中提到的对类型匹配的限制外,throw的操作数被完全视为调用中的函数参数或返回语句的操作数。

我见过一些代码库通过向异常而不是直接向对象抛出指针来解决这个问题,但就我个人而言,我首先会重新考虑您是否"需要"这样做。

当抛出一个对象时,会将该对象的副本复制到其他位置,以便可以继续堆栈展开,并且可以将副本传递给异常处理程序(通过引用不进行进一步的复制,或者通过值创建第二个副本)。

如果您使异常不可复制,则可以验证是否已创建副本。您将无法再投掷该类型的对象。

当实现复制您抛出的对象时,它会查看表达式的静态类型,而不是动态类型。这意味着在您的代码中,它看到您抛出了一个Exception,因此生成的副本是切片的一个示例(即,不复制完整的对象,只复制基类子对象)。

如果确保throw表达式的静态类型与完整对象的类型匹配,则可以避免切片,只需不将类型强制为Exception即可。

这应该打印"catchd:IllegalArgumentException"。

try
{
    IllegalArgumentException i;
    throw i;
}
catch(Exception& e)
{
    cout << "caught: ";
    e.print();
}

您看到的称为切片。

您显然已经习惯了多态性,在多态性中,您可以将指向子类的指针分配给指向基类的指针,并保留(子)类型和所有数据。毕竟,它只是一个被复制的指针,而不是对象本身。然而,当赋值直接在对象本身之间时,不会发生类似的情况;基类通常具有较短的实例,并且基类类型的变量后面可能没有可用空间(在您的情况下,在堆栈上;在其他人的情况下在堆上)。

因此,C++被定义为对对象执行切片。只有基类部分被复制,并且类型也被"降级"为基类。

您可以使用指针而不是引用来避免切片:

int main(int argc, char **argv)
{
    try
    {
        IllegalArgumentException* pIAE = new IllegalArgumentException();
        throw pIAE;
    }
    catch(IllegalArgumentException* i)
        {
            i->print();
    }
}

捕获的指针指向同一对象,因为复制构造函数不是隐式调用的。