“Successful engineering is all about understanding how things break or fail”
― Henry Petroski
During the discussion of the P1656 "Throws: Nothing" should be noexcept in Prague not all the important points were made. This paper brings additional information on use cases and P1656 breakages.
For a moment let's consider this
to be the part of function arguments. User kind-of provides it to the function, and in that case it may be a bad pointer. This bring UB to most of the member functions. For example std::vector::empty()
now has an UB if called on bad pointer and should not be marked with noexcept
.
Moreover, even pure functions without parameters may trigger UB as per stack overflow or signed integer overflow.
Treating noexcept
as 100% guarantee that there's no UB is wrong. Current rules "noexcept means no UB if you do not do ugly things" worked well, but the borders of that rule are quite vague.
Due to more than 10 years of experience with noexcept
users get used to the fact, that noexcept
means "bullet proof safety" (committee use "wide contract" term to describe that).
Now take a look at the following function:
constexpr basic_string_view(const CharT* s) noexcept;
The noexcept
means "bullet proof safety" habit provokes users to not read the documentation for the function. noexcept
increases the chances of misusing the function.
Big companies tend to have their own standard libraries. Some of them are written from scratch, some of them are copy-pasted and patched opensource solutions.
There are many workloads where reacting on a contract violation by throwing an exception is perfectly valid and makes a lot of sense. Here's a real world example:
std::terminate
or std::abort
looses the cache and in-process requests, forces the slow restart of the service and increased load on remaining instances. It is a perfectly valid desire to throw exceptions on contract violations rather than abort. In that case if someone updates of the database so that service starts calling optional::operator*()
on an empty optional we do not terminate. With termination even a small rate of bad requests that dereference empty optional could yield all the instances irresponsible. That's much worse than a ~1% speed degradation on checks and throws.Forcing the noexcept
policy for narrow contracts makes all those in-house standard libraries non conforming. C++ Standard does not satisfy the needs of those users any more.
We do not have Contracts as a language feature right now. Our understanding of Contracts is still vague, we have close to 0 in-field experience with them. But they are coming!
We've been living for 10 years with the "narrow contracts are not noexcept
" hoping that Contracts could do a good job in that place.
With P1656 adoption we're killing one of the use-cases for Contracts, probably making them quite useless for the Standard library. Even if no standard library vendor plans to use contracts right now, there are in house standard libraries that may benefit from Contracts in "Throws: Nothing." functions.
noexcept
everywhere is close to useless for the Standard LibraryStandard library sometimes implements different algorithms depending on the results of std::is_nothrow_*_v
for copy/move constructors, assignments and default constructors. In those cases we already have noexcept
and it improves the codegen.
But it does not improve codegen for other cases.
Due to the Standard library being mostly a header only library modern compilers deduce noexcept
specifiers by themselves. Moreover, standard C functions are nowadays mostly a built-in compiler intrinsics, so even for them noexcept
makes almost no sense. Other places that could benefit from noexcept
are already decorated by vendors (Standard permits that).
Additional noexcept
in the Standard won't give you a better codegen.
std::terminate
hellThis point is an outcome of bullet A. When users see noexcept
in the docs they tend to propagate that noexcept
to the enclosing function:
basic_string_view::basic_string_view(const CharT* s, size_t s) noexcept; T& std::vector::back() noexcept; // ... std::vector<char> get_data_from_cache(std::string_view username) noexcept; // vector::back() is noexcept, string_view constructor is noexcept => function is noexcept auto user_function1(std::vector<char>& v) noexcept { return std::string_view(&v.back(), 1); } auto user_function2(std::string_view username) noexcept { std::vector<char> v = get_data_from_cache(username); return user_function1(v); }
But that does not play well with refactoring. Users put themselves in to the "std::terminate
hell":
basic_string_view::basic_string_view(const CharT* s, size_t s) noexcept; T& std::vector::back() noexcept; // ... std::vector<char> get_data_from_cache(std::string_view username) noexcept; // Now we throw on empty vectors => function is NOT noexcept auto user_function1(std::vector<char>& v) { if (std::empty(v)) { throw std::out_of_range("user_function1 called with empty vector"); } return std::string_view(&v.back(), 1); } // Other code relied on user_function1 being noexcept. Now it is broken. auto user_function2(std::string_view username) noexcept { std::vector<char> v = get_data_from_cache(username); return user_function1(v); // sometimes terminates }
By making the functions with narrow contracts noexcept
we provoke users to mark their own narrow contract functions with noexcept
. That complicates the future refactoring and tends to be a bad practice.
The noexcept specifier allows us to express a contract explicitly in the specification, but we should not use it to confuse users and provide no-throwing guarantees for Undefined Behavior cases.
We are designing not only interfaces, we are designing the Standard Library as a whole.
By forcing the noexcept
instead of "Throws: Nothing" we:
Postpone P1656 "Throws: Nothing" should be noexcept adoption and revise it after the Contracts land into the WD.
TBD