Disclaimer: since this is likely to become an opinion based topic I would like to focus more on the factual side of things.
Prologue
In C++, we have the noexcept
specifier to declare that a function may (or may not) throw an expection. Furthermore, we have the noexcept
operator that tells us whether an expression can throw an exception or not.
Especially in TMP it can be a pain to specify whether a function can or cannot throw. Take std::invoke
for example:
template< class F, class... Args>
constexpr std::invoke_result_t<F, Args...> invoke(F&& f, Args&&... args) noexcept(noexcept(std::is_nothrow_invocable_v<F, Args...>));
It even needs a type trait for what basically results in:
auto invoke(F f, Args... args) noexcept(noexcept(f(args...)))
{
return f(args...);
}
Interestingly enough, the MSVC implementation even reverses the relationship between std::invoke
and std::is_nothrow_invocable
, basically doing
std::is_nothrow_invocable = noexcept(std::invoke(F, Args...))
and having a myriad of overloads for std::invoke
. Lets take a look at the case that handles a normal function call (e.g. lambda or namespace function):
template <class _Callable, class... _Types>
CONSTEXPR auto _CONCAT(NAME_PREFIX, invoke)(_Callable && _Obj, _Types && ... _Args)
noexcept(noexcept(_CONCAT(NAME_PREFIX, _Invoker) < _Callable, _Types... > ::_Call(_STD forward<_Callable>(_Obj), _STD forward<_Types>(_Args)...)))
-> decltype(_CONCAT(NAME_PREFIX, _Invoker) < _Callable, _Types... > ::_Call(_STD forward<_Callable>(_Obj), _STD forward<_Types>(_Args)...)) /* INVOKE a callable object */
{
return _CONCAT(NAME_PREFIX, _Invoker)<_Callable, _Types...>::_Call(_STD forward<_Callable>(_Obj), _STD forward<_Types>(_Args)...);
}
There are a lot of macros involved here but the gist of it is, that the function body has to be repeated 3 times:
- to accurately declare
noexcept
- to specify the return type (not sure why it's done that way, maybe to be explicit about it)
- the function body itself
Proposal
Now lets assume we extented the noexcept
specifier with the following syntax:
noexcept(auto)
The behavior is simple: resolve to noexcept(true)
if, and only if, noexcept(expr)
evaluates to true
for each expression expr
in the function's body. Otherwise noexcept(false)
.
I'm not that familiar with the standard so I'm not sure if the term expression is too loosely here.
The pseudo implementation above
auto invoke(F f, Args... args) noexcept(noexcept(f(args...)))
{
return f(args...);
}
would then become
auto invoke(F f, Args... args) noexcept(auto)
{
return f(args...);
}
Epilogue
I've only used std::invoke
as an example here but I'm certain there are more functions in the standard library (and in the wild) that could benefit from such a feature, especially in TMP.
Now to the questions:
- I don't see any compatibility issues if such a feature was added but I'm just a dude, so are there any problems in this regard?
- What are the caveats/disadvantages of this proposal?
- (optional) Where else would this come in handy within the standard library?
My last question is about compiler optimizations. After reading When should I really use noexcept? (especially the second answer) I was wondering what the compiler does if we declare a function noexcept
and throw inside that function. I know the standard requires a call to std::terminate
in this case, but how does a compiler implement this? I assume the compiler does something like this (going back to the std::invoke
example):
auto invoke(F f, Args... args) noexcept(noexcept(f(args...)))
{
try
{
return f(args...);
}
catch
{
std::terminate();
}
}
I'm far from beeing an expert on optimizations so I don't know how much the compiler optimizes, but I assume it's a lot. Nevertheless, the compiler is only required to do that handling because we could have lied when declaring our function noexcept
. However, if we let the compiler deduce the noexcept
ness it knows for certain and therefore doesn't need to catch anything. On the other hand, I assume the compiler already does that regardless. So
- Would this proposal affect performance?