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

拈花ヽ惹草 提交于 2019-11-29 06:12:47

throw implicitly copies, and consequently slices. Quoting C++11, §15.1/3:

A throw-expression initializes a temporary object, called the exception object, the type of which is determined by removing any top-level cv-qualifiers from the static type of the operand of throw and adjusting the type from “array of T” or “function returning T” to “pointer to T” or “pointer to function returning T”, respectively. The temporary is an lvalue and is used to initialize the variable named in the matching handler. If the type of the exception object would be an incomplete type or a pointer to an incomplete type other than (possibly cv-qualified) void the program is ill-formed. Except for these restrictions and the restrictions on type matching mentioned in 15.3, the operand of throw is treated exactly as a function argument in a call or the operand of a return statement.

I've seen a handful of codebases that work around this by throwing pointers to exceptions rather than objects directly, but personally I'd just reconsider your "need" to do this in the first place.

Jirka Hanika

What you see is called slicing.

You are apparently used to polymorphism, where you can assign a pointer to a subclass to a pointer to a base class, and you retain the (sub)type and all data. After all, it's been just a pointer being copied, not the object itself. However, nothing like this can happen when the assignment is directly between the objects themselves; the base class typically has shorter instances, and there may be no available room (in your case, on the stack; in somebody else's case, on the heap) following the variable of the base class type.

So, C++ is defined to perform slicing on the object. Only the base class part is copied, and the type is "downgraded" to the base class as well.

You can use a pointer instead of reference to avoid slicing:

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

The caught pointer points to the same object, because the copy constructor is not called implicitly.

When you throw an object a copy of that object is made to some other location so stack unwinding can proceed and the copy can be passed to the exception handler (either by reference so no further copies are made, or by value creating a second copy).

You can verify that a copy is made if you make your exception non-copyable. You will no longer be able to throw objects of that type.

When the implementation copies the object you throw, it looks at the static type of the expression, not the dynamic type. This means that in your code, it sees you're throwing an Exception, and so the resulting copy is an example of slicing (i.e. instead of copying the complete object, only a base class sub-object is copied).

You can avoid the slicing if you ensure that the static type of the throw expression matches the type of the complete object, which you can do by simply not forcing the type to Exception.

This should print "caught: IllegalArgumentException".

try
{
    IllegalArgumentException i;

    throw i;
}
catch(Exception& e)
{
    cout << "caught: ";
    e.print();
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!