Background: I refer to std::vector
's use of noexcept as "the vector
pessimization." I claim that the vector
pessimization is the only reason anyone ever cared about putting a noexcept
keyword into the language. Furthermore, the vector
pessimization applies only to the element type's move constructor. I claim that marking your move-assignment or swap operations as noexcept
has no "in-game effect"; leaving aside whether it might be philosophically satisfying or stylistically correct, you shouldn't expect it to have any effect on your code's performance.
Let's check a real library implementation and see how close I am to wrong. ;)
Vector reallocation. libc++'s headers use move_if_noexcept
only inside __construct_{forward,backward}_with_exception_guarantees
, which is used only inside vector reallocation.
Assignment operator for variant
. Inside __assign_alt
, the code tag-dispatches on is_nothrow_constructible_v<_Tp, _Arg> || !is_nothrow_move_constructible_v<_Tp>
. When you do myvariant = arg;
, the default "safe" approach is to construct a temporary _Tp
from the given arg
, and then destroy the currently emplaced alternative, and then move-construct that temporary _Tp
into the new alternative (which hopefully won't throw). However, if we know that the _Tp
is nothrow-constructible directly from arg
, we'll just do that; or, if _Tp
's move-constructor is throwing, such that the "safe" approach isn't actually safe, then it's not buying us anything and we'll just do the fast direct-construction approach anyway.
Btw, the assignment operator for optional
does not do any of this logic.
Notice that for variant
assignment, having a noexcept move constructor actually hurts (unoptimized) performance, unless you have also marked the selected converting constructor as noexcept
! Godbolt.
(This experiment also turned up an apparent bug in libstdc++: #99417.)
string
appending/inserting/assigning. This is a surprising one. string::append
makes a call to __append_forward_unsafe
under a SFINAE check for __libcpp_string_gets_noexcept_iterator
. When you do s1.append(first, last)
, we'd like to do s1.resize(s1.size() + std::distance(first, last))
and then copy into those new bytes. However, this doesn't work in three situations: (1) If first, last
point into s1
itself. (2) If first, last
are exactly input_iterator
s (e.g. reading from an istream_iterator
), such that it's known impossible to iterate the range twice. (3) If it's possible that iterating the range once could put it into a bad state where iterating the second time would throw. That is, if any of the operations in the second loop (++
, ==
, *
) are non-noexcept. So in any of those three situations, we take the "safe" approach of constructing a temporary string s2(first, last)
and then s1.append(s2)
. Godbolt.
I would bet money that the logic controlling this string::append
optimization is incorrect. (EDIT: yes, it is.) See "Attribute noexcept_verify
" (2018-06-12). Also observe in that godbolt that the operation whose noexceptness matters to libc++ is rv == rv
, but the one it actually calls inside std::distance
is lv != lv
.
The same logic applies even harder in string::assign
and string::insert
. We need to iterate the range while modifying the string. So we need either a guarantee that the iterator operations are noexcept, or a way to "back out" our changes when an exception is thrown. And of course for assign
in particular, there's not going to be any way to "back out" our changes. The only solution in that case is to copy the input range into a temporary string
and then assign from that string
(because we know string::iterator
's operations are noexcept, so they can use the optimized path).
libc++'s string::replace
does not do this optimization; it always copies the input range into a temporary string
first.
function
SBO. libc++'s function
uses its small buffer only when the stored callable object is_nothrow_copy_constructible
(and of course is small enough to fit). In that case, the callable is treated as a sort of "copy-only type": even when you move-construct or move-assign the function
, the stored callable will be copy-constructed, not move-constructed. function
doesn't even require that the stored callable be move-constructible at all!
any
SBO. libc++'s any
uses its small buffer only when the stored callable object is_nothrow_move_constructible
(and of course is small enough to fit). Unlike function
, any
treats "move" and "copy" as distinct type-erased operations.
Btw, libc++'s packaged_task
SBO doesn't care about throwing move-constructors. Its noexcept move-constructor will happily call the move-constructor of a user-defined callable: Godbolt. This results in a call to std::terminate
if the callable's move-constructor ever actually does throw. (Confusingly, the error message printed to the screen makes it look as if an exception is escaping out the top of main
; but that's not actually what's happening internally. It's just escaping out the top of packaged_task(packaged_task&&) noexcept
and being halted there by the noexcept
.)
Some conclusions:
To avoid the vector
pessimization, you must declare your move-constructor noexcept. I still think this is a good idea.
If you declare your move-constructor noexcept, then to avoid the "variant
pessimization," you must also declare all your single-argument converting constructors noexcept. However, the "variant
pessimization" merely costs a single move-construct; it does not degrade all the way into a copy-construct. So you can probably eat this cost safely.
Declaring your copy constructor noexcept
can enable small-buffer optimization in libc++'s function
. However, this matters only for things that are (A) callable and (B) very small and (C) not in possession of a defaulted copy constructor. I think this describes the empty set. Don't worry about it.
Declaring your iterator's operations noexcept
can enable a (dubious) optimization in libc++'s string::append
. But literally nobody cares about this; and besides, the optimization's logic is buggy anyway. I'm very much considering submitting a patch to rip out that logic, which will make this bullet point obsolete. (EDIT: Patch submitted, and also blogged.)
I'm not aware of anywhere else in libc++ that cares about noexceptness. If I missed something, please tell me! I'd also be very interested to see similar rundowns for libstdc++ and Microsoft.
noexcept
and a throwing move constructor would break that. – Interglacial