I'm trying to experiment with code by myself here on different compilers. I've been trying to lookup the advantages of disabling exceptions on certain functions (via the binary footprint) and to compare that to functions that don't disable exceptions, and I've actually stumbled onto a weird case where it's better to have exceptions than not.
I've been using Matt Godbolt's Compiler Explorer to do these checks, and it was checked on x86-64 clang 12.0.1 without any flags (on GCC this weird behavior doesn't exist).
Looking at this simple code:
auto* allocated_int()
{
return new int{};
}
int main()
{
delete allocated_int();
return 0;
}
Very straight-forward, pretty much deletes an allocated pointer returned from the function allocated_int()
.
As expected, the binary footprint is minimal, as well:
allocated_int(): # @allocated_int()
push rbp
mov rbp, rsp
mov edi, 4
call operator new(unsigned long)
mov rcx, rax
mov rax, rcx
mov dword ptr [rcx], 0
pop rbp
ret
Also, very straight-forward.
But the moment I apply the noexcept
keyword to the allocated_int()
function, the binary bloats. I'll apply the resulting assembly here:
allocated_int(): # @allocated_int()
push rbp
mov rbp, rsp
sub rsp, 16
mov edi, 4
call operator new(unsigned long)
mov rcx, rax
mov qword ptr [rbp - 8], rcx # 8-byte Spill
jmp .LBB0_1
.LBB0_1:
mov rcx, qword ptr [rbp - 8] # 8-byte Reload
mov rax, rcx
mov dword ptr [rcx], 0
add rsp, 16
pop rbp
ret
mov rdi, rax
call __clang_call_terminate
__clang_call_terminate: # @__clang_call_terminate
push rax
call __cxa_begin_catch
call std::terminate()
Why is clang doing this extra code for us? I didn't request any other action but calling new()
, and I was expecting the binary to reflect that.
Thank you for those who can explain!
That noexcept keyword is tricky, but just know that if you use it, your coding world will spin faster.
noexcept is an improved version of throw(), which is deprecated in C++11. Unlike pre-C++17 throw(), noexcept will not call std::unexpected, may or may not unwind the stack, and will call std::terminate, which potentially allows the compiler to implement noexcept without the runtime overhead of throw().
The noexcept operator performs a compile-time check that returns true if an expression is declared to not throw any exceptions. It can be used within a function template's noexcept specifier to declare that the function will throw exceptions for some types but not others.
In general, you should use noexcept when you think it will actually be useful to do so. Some code will take different paths if is_nothrow_constructible is true for that type. If you're using code that will do that, then feel free to noexcept appropriate constructors.
The standard library’s policy is to use noexcept only on functions that must not throw or fail. Functions that are potentially throwing but do not actually throw exceptions (due to implementation) typically are not marked as noexcept. Use the noexcept specifier in specific cases where you want to express a no-fail or no-throw guarantee.
In contrast, noexcept (false) means that the function may throw an exception. The noexcept specification is part of the function type but can not be used for function overloading. There are two good reasons for the use of noexcept: First, an exception specifier documents the behaviour of the function.
It can be used in a noexcept specifier of a function template to declare that the function may throw exceptions depending on the current type. To make my description clear here is a simple example of a function template which copies it return value. Of course, the most interesting line in this example is the line (1).
The noexcept specification is equivalent to the noexcept (true) specification. throw () is equivalent to noexcept (tru e) but was deprecated with C++11 and will be removed with C++20. In contrast, noexcept (false) means that the function may throw an exception.
Why is clang doing this extra code for us?
Because the behaviour of the function is different.
I didn't request any other action but calling new()
By declaring the function noexcept
, you've requested std::terminate
to be called in case an exception propagates out of the function.
allocated_int
in the first program never calls std::terminate
, while
allocated_int
in the second program may call std::terminate
. Note that the amount of added code is much less if you remember to enable the optimiser. Comparing non-optimised assembly is mostly futile.
You can use non-throwing allocation to prevent that:
return new(std::nothrow) int{};
It's indeed an astute observation that doing potentially throwing things inside non-throwing function can introduce some extra work that wouldn't need to be done if the same things were done in a potentially throwing function.
I've been trying to lookup the advantages of disabling exceptions on certain functions
The advantage of using non-throwing is potentially realised where such function is called; not within the function itself.
Without nothrow
, your function just acts as a front end to the allocation function you call. It doesn't have any real behavior of its own. In fact, in a real executable, if you do link-time optimization there's a pretty good chance that it'll completely disappear.
When you add noexcept
, your code is silently transformed into something roughly like this:
auto* allocated_int()
{
try {
return new int{};
}
catch(...) {
terminate();
}
}
The extra code you see generated is what's needed to catch the exception and call terminate
when/if needed.
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