We often heard a lot of zero-cost/zero-overhead people who claim C++ EH is zero-cost at happy path without any concrete measurement.
However in reality, C++ EH hurts optimizations and bloats binary size. There are no such thing called zero-cost abstractions.
Here is an example, I recently encounter an issue with stdout in mingw-w64-crt. Usually libc vendors won't mark their functions as noexcept, even that code is written in C and can never,ever throw exceptions. https://github.com/mirror/mingw-w64/blob/master/mingw-w64-headers/crt/stdio.h
_CRTIMP FILE *__cdecl __acrt_iob_func(unsigned index);
#ifndef _STDIO_DEFINED
#ifdef _WIN64
_CRTIMP FILE *__cdecl __iob_func(void);
#define _iob __iob_func()
#else
#ifdef _MSVCRT_
extern FILE _iob[]; /* A pointer to an array of FILE */
#define __iob_func() (_iob)
#else
extern FILE (* __MINGW_IMP_SYMBOL(_iob))[]; /* A pointer to an array of FILE */
#define __iob_func() (* __MINGW_IMP_SYMBOL(_iob))
#define _iob __iob_func()
#endif
#endif
#endif
#ifndef _STDSTREAM_DEFINED
#define _STDSTREAM_DEFINED
#define stdin (__acrt_iob_func(0))
#define stdout (__acrt_iob_func(1))
#define stderr (__acrt_iob_func(2))
#endif
We can see stdout macro is a function call in the mingw-w64-crt. My code is written like this, the c_io_observer just holds a stdout.
inline c_io_observer c_stdout() noexcept
{
return {stdout};
}
Then print function by default prints to stdout.
print("Hello World\n");
With EH enabled, clang treats stdout can throw exceptions (because mingw-w64 does not mark that function with noexcept), since print will by default print to c_stdout(), clang will generate a lot of __clang_terminate() in my code and it hurts inlining and other optimizations. I was shocked how bad the assembly is, the assembly goes from 500 lines to 800 lines + print functions sometimes won't be inlined any more. That performance hit is HUGE. Unfortunately, a lot of libc implementations (like musl libc, mlibc) do not mark their C functions as noexcept and create a huge amount of bloat and slow down in C++. However, I do come up a solution, by hacking around windows crt which is this noexcept_call implementation. It is implemented as
template<typename R, typename ...Args>
struct make_noexcept
{};
template<typename R, typename ...Args>
struct make_noexcept<R(Args...)> { using type = R(Args...) noexcept; };
template<typename R, typename ...Args>
struct make_noexcept<R(Args...) noexcept> { using type = R(Args...) noexcept; };
template<typename R, typename ...Args>
using make_noexcept_t = typename make_noexcept<R,Args...>::type;
template<typename F>
requires std::is_function_v<F>
#if __has_cpp_attribute(gnu::always_inline)
[[gnu::always_inline]]
#elif __has_cpp_attribute(msvc::forceinline)
[[msvc::forceinline]]
#endif
inline constexpr auto noexcept_cast(F* f) noexcept
{
#if __cpp_lib_bit_cast >= 201806L
return __builtin_bit_cast(make_noexcept_t<F>*,f);
#else
return reinterpret_cast<make_noexcept_t<F>*>(f);
#endif
}
template<typename F,typename... Args>
requires std::is_function_v<F>
#if __has_cpp_attribute(gnu::always_inline)
[[gnu::always_inline]]
#elif __has_cpp_attribute(msvc::forceinline)
[[msvc::forceinline]]
#endif
inline
#if __cpp_if_consteval >= 202106L || __cpp_lib_is_constant_evaluated >= 201811
constexpr
#endif
decltype(auto) noexcept_call(F* f,Args&& ...args) noexcept
{
#if __cpp_if_consteval >= 202106L
if consteval
{//write your own forward function since std::forward is not freestanding and toolchains might not provide utility header
return f(::fast_io::freestanding::forward<Args>(args)...); //EH unwinding does not matter here
}
else
{
return noexcept_cast(f)(::fast_io::freestanding::forward<Args>(args)...);
}
#else
#if __cpp_lib_is_constant_evaluated >= 201811
if (std::is_constant_evaluated())
return f(::fast_io::freestanding::forward<Args>(args)...); //EH unwinding does not matter here
else
#endif
return noexcept_cast(f)(::fast_io::freestanding::forward<Args>(args)...);
#endif
}
So when we call
noexcept_call(some_libc_function,arg1,arg2);
instead of
some_libc_function(arg1,arg2);
It will force the compiler to treat some_libc_function function as noexcept function (which is what libcs functions are used in the C way).
Finally the fix is https://github.com/tearosccebe/fast_io/commit/86ef492439dd3db1fe4802fb0af25f417a683692#diff-f2d17467112ab19026deacb580c4d81e36af61ed80344b4cfa846fbec9ce02d2
#if __has_cpp_attribute(gnu::const)
[[gnu::const]]
#endif
#if __has_cpp_attribute(gnu::always_inline)
[[gnu::always_inline]]
#elif __has_cpp_attribute(msvc::forceinline)
[[msvc::forceinline]]
#endif
inline FILE* wincrt_iob_func() noexcept
{
#if defined(__iob_func)
return __iob_func();
#else
return noexcept_call(__iob_func);
#endif
}
namespace win32
{
#if __has_cpp_attribute(gnu::const)
[[gnu::const]]
#endif
inline FILE* wincrt_acrt_iob_func(unsigned index) noexcept
{
#if defined(_MSC_VER) || defined(_UCRT)
return noexcept_call(__acrt_iob_func,index);
#else
return ::fast_io::win32::wincrt_iob_func()+index;
#endif
}
}
and use ::fast_io::win32::wincrt_acrt_iob_func(0) to replace stdin. ::fast_io::win32::wincrt_acrt_iob_func(1) to replace stdout. ::fast_io::win32::wincrt_acrt_iob_func(2) to replace stderr. I've started to guard all libc functions calls with noexcept_call since not marking them creates binary bloat and huge performance slow down.
I really hope a working compiler that implements herbceptions (P0709) could come up asap, so i can do -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables -fno-ident forever and ignoring problems like this.