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