c++gccterminatenoexcept

Understand what really happens when calling a throwing function in a noexcept function


See the following code containing 3 implementations of a function calling another throwing function.

# include <stdexcept>

void f()
{
    throw std::runtime_error("");
}

void g1()
{
    f();
}

void g2() noexcept
{
    f();
}

void g3() noexcept
{
    try{ f(); } catch(...){ std::terminate(); }
}

int main()
{
    return 0;
}

In my understanding of the noexcept specification, g2 and g3 are strictly equivalent. But, when I compile it in Compiler Explorer with GCC, the generated code is strictly equivalent for g1 and g2, but not for g3:

g1():
        push    rbp
        mov     rbp, rsp
        call    f()
        nop
        pop     rbp
        ret
g2():
        push    rbp
        mov     rbp, rsp
        call    f()
        nop
        pop     rbp
        ret
g3():
        push    rbp
        mov     rbp, rsp
        call    f()
        jmp     .L9
        mov     rdi, rax
        call    __cxa_begin_catch
        call    std::terminate()
.L9:
        pop     rbp
        ret

Why ?


Solution

  • The way exceptions are implemented in GCC, there is no need to emit extra code for noexcept and throws checks. (This is called zero-cost exceptions, see also this and this.) The compiler creates several tables with information about all functions, their stack variables and exceptions they are allowed to throw. When an exception is thrown, this info is used to unwind the call stack. It is during the stack unwinding the standard library will notice that there is a noexcept entry in the stack and call std::terminate. So there is a difference between g1 and g2, but it's not in the .text section of the generated binary, but somewhere in .eh_frame, eh_frame_hdr or .gcc_except_table. These are not shown by godbolt.org.

    If you execute these functions wrapped in try-catch from main, you will observe that indeed, despite the code of g2 not having anything extra compared to g1, the execution will not reach the catch clause in main and std::terminate earlier. Roughly speaking, this std::terminate will happen when executing throw in f.

    As for why g3 code is different, it's probably because the optimizer couldn't look through all this involved exception handling logic and therefore didn't change the initially generated code much.

    EDIT: Actually godbolt.org can display related ASM directives that populate the tables if you disable the filter for directives.

    g1():
    .LFB1414:
            .loc 1 9 1 is_stmt 1
            .cfi_startproc
            push    rbp
            .cfi_def_cfa_offset 16
            .cfi_offset 6, -16
            mov     rbp, rsp
            .cfi_def_cfa_register 6
            .loc 1 10 6
            call    f()
            .loc 1 11 1
            nop
            pop     rbp
            .cfi_def_cfa 7, 8
            ret
            .cfi_endproc
    

    vs

    g2():
    .LFB1415:
            .loc 1 14 1
            .cfi_startproc
            .cfi_personality 0x3,__gxx_personality_v0
            .cfi_lsda 0x3,.LLSDA1415
            push    rbp
            .cfi_def_cfa_offset 16
            .cfi_offset 6, -16
            mov     rbp, rsp
            .cfi_def_cfa_register 6
            .loc 1 15 6
            call    f()
            .loc 1 16 1
            nop
            pop     rbp
            .cfi_def_cfa 7, 8
            ret
            .cfi_endproc
    

    Notice the extra lines

            .cfi_personality 0x3,__gxx_personality_v0
            .cfi_lsda 0x3,.LLSDA1415