6

When an exception is thrown in C++ and the stack is unwound, how is is the correct handler (catch clause) picked to handle the exception?

void f1()
{
    throw 1;
}

void f2()
{
    try
    {
        f1();
    }
    catch(const char* e)
    {
        std::cout << "exc1";
    }
}

...
try
{
    f2();
}
catch(int& e)
{
    std::cout << "exc2";
}
...

For example this code unsurprisingly prints "exc2" because catch(int& e) is capable of handling the 1 int typed object.

What I don't understand is, how can this be resolved statically? Or is it resolved dynamically? Is type information propagated with the exception?

imreal
  • 10,178
  • 2
  • 32
  • 48

3 Answers3

3

As for most things, the C++ standard doesn't specify an implementation, but constrains valid implementations. There won't be a universal answer that knows about the specifics.

The Itanium ABI is a popular ABI that provides language-agnostic exception support. In that implementation, the unwinding API invokes the personality function of the stack frame, which receives the exception context, an exception structure and a reference to the exception handling table that guides catch behavior. The personality function is looked up based on the call's return address in the program's exception tables; it is assumed that the only instructions that can initiate an exception are call instructions. (GCC has extensions that allow to throw from signal handlers by enabling "non-call exceptions".) Compilers that use the Itanium ABI provide a personality function that knows how to check the runtime type of exception objects, and they compare that type to elements of the exception table.

There are other ways to do that. For instance, on 32-bit Windows, exception handling works by setting up handler functions, on the stack, as linked list items. These linked list nodes contain the address of exception handlers, and they use an EXCEPTION_RECORD to figure out where to go from there. I'm a bit sparse on the details myself, unfortunately.

The Itanium ABI, by the way, is not exclusive to C++. For instance, on Apple platforms, Objective-C code uses the Itanium ABI for exceptions as well. This has the interesting property that a catch-all clause in either language will catch exceptions from the other language.

zneak
  • 134,922
  • 42
  • 253
  • 328
1

You could say that "the type information is propagated with the exception", but technically that's not true. When you throw something, that object gets copied/moved and the compiler will look for a appropriate catch clause in the order that they appear, where the object will get copied/moved to:

The exception is a match if any of the following is true:

  • E and T are the same type (ignoring top-level cv-qualifiers on T)

  • T is an lvalue-reference to (possibly cv-qualified) E

  • T is an unambiguous public base class of E

  • T is a reference to an unambiguous public base class of E

  • T is (possibly cv-qualified) U or const U& (since C++14), and U is a pointer or pointer to member (since C++17) type, and E is also a pointer or pointer to member (since C++17) type that is implicitly convertible to U by one or more of

    • a standard pointer conversion other than one to a private, protected, or ambiguous base class
    • a qualification conversion
    • a function pointer conversion (since C++17)
    • T is a pointer or a pointer to member or a reference to a const pointer (since C++14), while E is std::nullptr_t.

Additionally, if there is a catch-all clause (catch (...)), then it is taken if any of the above points do not apply. If no catch clause is suitable, the exception continues its way up the call stack.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162
  • I still don't understand how can that be resolved at runtime without type information being propagated. – imreal May 23 '17 at 17:56
  • @imreal You can imagine it almost like a function call: The first match `catch` clause get's selected by overload resolution, and then it is "called". You don't need RTTI for function calls, it's the same here. – Rakete1111 May 23 '17 at 17:57
  • A function overload resolution can easily be handled at compile time by creating a list of candidates using koenig lookup. The handler candidates though, vary depending of the state of the stack at the time of the exception, how can that be known at compile time? – imreal May 23 '17 at 18:02
  • @imreal Why would they vary? They don't change depending on the stack if I'm understanding you correctly. – Rakete1111 May 23 '17 at 18:04
  • So in the example provided, `f1()` is called from `f2()` which in turn gets called by `main()` with the only handler for `int`, but it could have been called from a hypothetical `f3()` that also has a handler for `int` and beat the handler in `main()`. How does the compiler know where in the stack to stop? At compile time? – imreal May 23 '17 at 18:07
  • @imreal but if the catch handler is in a different compilation unit then overload resolution cannot resolve that which the compiler cannot see. – Richard Critten May 23 '17 at 18:17
  • @RichardCritten exactly my point. How does it happen at compile time? – imreal May 23 '17 at 18:20
  • @imreal The exception just goes up the call stack. It stops when an appropriate catch clause is found. That happens at runtime though. – Rakete1111 May 23 '17 at 18:25
  • @Rakete1111 Exactly, so the object must carry type information right? – imreal May 23 '17 at 18:27
  • @imreal No. Taking the analogy again: Calling a function doesn't need RTTI. It is the same thing. The thrown object is being catched by a catch clause that can (see list). You don't need type information for that, just like overload resolution. – Rakete1111 May 23 '17 at 19:45
  • @Rakete1111 Do we agree that the handler is selected at runtime based on the state of the stack at the time the exception is thrown? Because at compile time it is not possible to know who will call a function? – imreal May 23 '17 at 20:01
  • @imreal - worth a read: https://monoinfinito.wordpress.com/2013/07/25/c-exceptions-under-the-hood-appendix-iii-rtti-and-exceptions-orthogonality/ – Richard Critten May 23 '17 at 20:03
  • 2
    @Rakete1111, I would like to challenge you to name one C++ implementation that uses *no* form of RTTI at all to catch exceptions. The problem of identifying which types of objects can be thrown on a code path is uncomputable, so there has to be a way to compare arbitrary types, and I'm not sure how you can do that without at least *some* form of type information. That doesn't have to be the entire set of RTTI-related operations like `dynamic_cast`, but there has to be something. – zneak May 23 '17 at 20:39
  • @zneak Good point. I was pretty sure that RTTI was no needed at all. Well ok, thanks for clarifying :) – Rakete1111 May 24 '17 at 08:50
0

As some answers and comments have stated, the compiler does generate runtime type information RTTI for exception types that the exception dispatcher uses to match the right handler to an exception object.

As you can see from a modified version of my example here:

https://godbolt.org/g/dyheBZ

The compiler (gcc) generates a typeinfo block in section .rodata for objects that are used in throw or catch statements:

    .size   typeinfo for A, 16
typeinfo for A:
    .quad   vtable for __cxxabiv1::__class_type_info+16
    .quad   typeinfo name for A
    .weak   typeinfo name for A
    .section        .rodata._ZTS1A,"aG",@progbits,typeinfo name for A,comdat
    .type   typeinfo name for A, @object
    .size   typeinfo name for A, 3
typeinfo name for A:
    .string "1A"
    .text
    .type   __static_initialization_and_destruction_0(int, int), @function

And the throw A(); statement uses this information to throw the exception:

    mov     edi, 1
    call    __cxa_allocate_exception
    mov     edx, 0
    mov     esi, OFFSET FLAT:typeinfo for A
    mov     rdi, rax
    call    __cxa_throw
imreal
  • 10,178
  • 2
  • 32
  • 48