Static type of the exception object

2019-07-05 02:50发布

问题:

I read the following from C++ Primer (5th edition, Section 18.1.1): "When we throw an expression, the static, compile-time type of that expression determines the type of the exception object." So I tried the following code:

#include <iostream>

class Base{
  public:
  virtual void print(std::ostream& os){os << "Base\n";}
};

class Derived: public Base{
  public:
  void print(std::ostream& os){os << "Derived\n";}
};

int main(){
  try{
    Derived d;
    Base &b = d;
    b.print(std::cout); //line 1
    throw b;
  }
  catch(Base& c){
    c.print(std::cout); //line 2
  }
return 0;
}

which gives me the following output:

Derived
Base

I think I understand why this output is expected: at line 1, we have dynamic binding. Now when we throw b, it is based on the static type of b, which means both the static type and the dynamic type of c is Base&, and therefore we see the result at line 2.

However, if I were to use a pointer, instead of a reference:

 int main(){
  try{
    Derived d;
    Base *b = &d;
    b->print(std::cout); //line 1
    throw b;
  }
  catch(Base* c){
    c->print(std::cout); //line 2
  }
return 0;
}

the output now becomes:

Derived
Derived

which seems to imply that the static type of c is Base*, but the dynamic type of c is Derived*, why? Shouldn't both the static and the dynamic types of c be Base*?

回答1:

When we throw an expression, the static, compile-time type of that expression determines the type of the exception object

The above is entirely true. What you forget, is that pointers are objects too. And when you throw a pointer, that's your exception object.

The object you should have dynamically1 allocated is still pointed to by that Base* pointer. And no slicing occurs on it, because there is no attempt to copy it. As such, dynamic dispatch via-pointer accesses a Derived object, and that object will use the overriding function.

This "discrepancy" is why it is usually best to construct the exception object in the throw expression itself.


1 That pointer points to a local object, you did a big no no there and got yourself a dangling pointer.



回答2:

In first case you are throwing a fresh instance of Base class invoking a copy constructor because you are passing a reference to Base into throw operator.

In second case you are throwing a pointer to a stack-allocated object of type Derived that goes out of scope when exception is thrown so then you capture and then dereference a dangling pointer causing Undefined Behavior.



回答3:

First scenario

I think that if you add some prints to your classes, you could see a clearer picture:

struct Base {
    Base() { std::cout << "Base c'tor\n"; }
    Base(const Base &) { std::cout << "Base copy c'tor\n"; }

    virtual void print(std::ostream& os) { std::cout << "Base print\n"; }
};

struct Derived: public Base {
    Derived() { std::cout << "Derived c'tor\n"; }
    Derived(const Derived &) { std::cout << "Derived copy c'tor\n"; }

    virtual void print(std::ostream& os) { std::cout << "Derived print\n"; }
};

And the output is:

Base c'tor
Derived c'tor
Derived print
throwing // Printed right before `throw b;` in main()
Base copy c'tor
Base print

As you can see, when calling throw b; there is a copy construction of a different temporary Base object for the exception. From cppreference.com:

First, copy-initializes the exception object from expression

This copy-initialization slices the object, as if you assigned Base c = b

Second scenario

First, you are throwing a pointer to a local object, causing undefined behavior, please avoid that by all means!

Let's say you fix that, and you throw a dynamically allocated pointer, it works since you are throwing a pointer, which doesn't affect the object and preserves dynamic type information.