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*?
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.
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.
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.