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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With