4

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

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
yuanlu0210
  • 193
  • 6

3 Answers3

3

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.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • Thanks!! I see the dangling pointer now. What about the first case? Did I have a "dangling reference" or not? – yuanlu0210 Oct 22 '17 at 08:15
  • 2
    @ivy - No. In the first case the exception object is a `Base` (The type `T&` is adjusted to `T` in most expressions). So the referred to object was *copied*, and hence I told you it was sliced. – StoryTeller - Unslander Monica Oct 22 '17 at 08:19
  • Can all these be considered as consequences of the fact that when we use the throw operation, the first step would be to copy initialize a temporary exception object from the expression? That is, it is as if I did `Base e = b; e.print(std::cout);` in the first case (here b is a reference) and `Base* e = b; e->print(std::cout);` in the second case (here b is a pointer). Is this the right way of thinking? (Still cannot get my ahead around the static vs dynamic typing...). – yuanlu0210 Oct 22 '17 at 08:45
  • @ivy - You can think of this as what's happening. There's only one minor point, and that is how you catch. If you do so by value, and not by reference, the exception object is copied again into the catch clause. – StoryTeller - Unslander Monica Oct 22 '17 at 08:50
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.

user7860670
  • 35,849
  • 4
  • 58
  • 84
0

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.

Daniel Trugman
  • 8,186
  • 20
  • 41