C++: Throwing a derived class by reference does no

2020-02-10 19:00发布

I want to throw my own exceptions with the base class Exception. There is a virtual method print which will be overwritten by the subclasses. I only catch the type Exception& and use print to get the specific error. The problem is that once I throw a reference of a subclass it is trated as if it were the base class.

Here is an example:

#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();
    }
}

The output of this example is:

ref.print: IllegalArgumentException
catched: Exception

Using a reference should result in the print method from the derived class being used. Inside the try block the reference does use it. Why doesn't the catched Exception& act like an IllegalArgumentException and how can I get that behavior?

The following code seems to do what it is supposed to:

try
{
    IllegalArgumentException i;
    Exception* pointer = &i;

    throw pointer;
}
catch(Exception* e)
{
    cout << "pointer catched: ";
    e->print();
}

but doesn't the pointer become possibly invalid outside the scope of the try block? It would then be risky to do this and if I allocate memory on the heap to get around that problem I have the responsibility for the deletion inside the catch block which isn't pretty either. So how would you solve the problem?

4条回答
我想做一个坏孩纸
2楼-- · 2020-02-10 19:42

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();
}
查看更多
\"骚年 ilove
3楼-- · 2020-02-10 19:48

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.

查看更多
Explosion°爆炸
4楼-- · 2020-02-10 19:55

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.

查看更多
够拽才男人
5楼-- · 2020-02-10 19:59

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.

查看更多
登录 后发表回答