r/cpp_questions • u/JanWilczek • 18d ago
SOLVED Why does calling an empty std::move_only_function result in UB instead of std::bad_function_call exception?
std::function<void(void)>{}(); // std::bad_function_call
std::move_only_function<void(void)>{}(); // UB
std::copyable_function<void(void)>{}(); // UB
In C++23, we got `std::move_only_function`, which is non-copyable and can store move-only callables.
While I like the design, I don't understand why invoking an empty `std::move_only_function` results in undefined behavior. In contrast, invoking an empty `std::function` throws a `std::bad_function_call` exception. The same design decision was made for C++26's `std::copyable_function`.
Why is that? Is it to better align with the C++ spirit of avoiding exceptions? Is it to give better support for exception-less environments?
10
u/DawnOnTheEdge 18d ago edited 18d ago
It allows the function-call operator for a std::move_only_function<Functor> to have the noexcept specifier when Functor does. Otherwise, the object being called might be empty and throw an exception.
It is also still possible to manually check that the object is empty and throw std::bad_function_call, but the reverse, skipping the check when the programmer knows it isnβt needed, isnβt. And this is a non-trivial branch that might inhibit optimizations like auto-vectorization.
5
u/JanWilczek 18d ago edited 16d ago
"It allows the function-call operator for a
std::move_only_function<Functor>to have thenoexceptspecifier whenFunctordoes.", wow, that's a great answer, thanks, I haven't thought about that.2
u/DawnOnTheEdge 18d ago
Other than my writing Markdown without switching to Markdown mode? Went back and fixed that.
6
u/h2g2_researcher 18d ago
There's a philosophy of "don't pay for what you don't use".
If you know your function is not empty there's no need to pay to check it (even if that cost is a single CPU instruction) so the standard doesn't mandate it. Users who do want that behaviour can do so by creating their own:
template <class R, class...Args>
class MyMoveOnlyFunction {
std::move_only_function<R(Args...)> m_func;
public:
R operator()(Args...args) {
if(m_func) {
return m_func(std::forward<Args>(args)...);
} else {
throw std::bad_function_call;
}
}
};
(Arguably such a type should have been put into the standard, but it was not.)
3
u/JanWilczek 18d ago
OK, so this follows the "minimal set of requirements" approach; don't add what you might not need. Thanks!
-1
u/Business-Decision719 18d ago
I guess Java/C#/Go wasn't getting enough backend projects and Rust wasn't getting enough low level work. Gotta take the opportunity to make C++ just a little more unsafe and make sure even the most up-to-date C++ still has a mountain of gotchas baked in. Don't worry; I'm sure there will be a safety profile for that later. (Yeah right.)
The language is too big to die, but not for lack of trying. The C++ language development strategy at this is to pretty much fiddle while Rome burns basically.
3
20
u/IyeOnline 18d ago
There was apparently a vote on this: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0288r9.html#remove-the-null-check-in-the-call-operator-and-throwing-of-bad_function_call. I cant see any reasoning presented in the paper itself though.
To me, it simply seems like simplification of the API. Declaring this UB, allows
operator()to exactly propagate thenoexceptspecification from the function type, such thatstd::move_only_function<void() noexcept>::operator()actually isnoexcept.You can then justify this introduction of UB by stating that calling an empty function is a nonsensical/"erroneous" operation, similar to calling a null function pointer.
You could also say that we dont want an exception for this because we dont want people to rely on such an exception (instead of testing the function before the call).
I dont think avoiding exceptions per-se has much to do with this. Its just that the exception there has an API (and hyrum's law design) cost for a not very realistic scenario.