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

670 Views Asked by At

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 ?

3

There are 3 best solutions below

0
Alexey Veleshko On BEST ANSWER

The way exceptions are implemented in GCC, there is no need to emit extra code for noexcept and throws checks. 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
3
J0llyver On

Reading the noexcept reference on cppreference.com I came to the conclusion that the compiled code is not affects by noexcept

It is only a compile time evaluation and therefore does not influence the output of the compiler if just used on its own.

0
yyyy On

You can see the difference if and only if the function actually needs to do something, either to catch the exception or to call a destructor, when an execption is thrown.

https://godbolt.org/z/PrqEeKrqP

Here's an example.

If every function called in a block is marked as noexcept, or if the calling function itself is noexcept, the compiler can safely omit all exception handling logic. Otherwise, a hidden catch block is generated to call destructors.

Note that I compile the example code with -shared flag to avoid inlining.