Move operations should be noexcept
; in the first place for intuitive and reasonable semantics. The second argument is runtime performance. From the Core Guidelines, C.66, "Make move operations noexcept":
A throwing move violates most people’s reasonably assumptions. A non-throwing move will be used more efficiently by standard-library and language facilities.
The canonical example for the performance-part of this guideline is the case when std::vector::push_back
or friends need to grow the buffer. The standard requires a strong exception guarantee here, and this can only move-construct the elements into the new buffer if this is noexcept
- otherwise, it must be copied. I get that, and the difference is visible in benchmarks.
However, apart from this, I have a hard time finding real-world evidence of the positive performance impact of noexcept
move semantics. Skimming through the standard library (libcxx
+ grep
), we see that std::move_if_noexcept
exists, but it's almost not used within the library itself. Similarly, std::is_noexcept_swappable
is merely used for fleshing out conditional noexcept
qualifiers. This doesn't match existing claims, for example this one from "C++ High Performance" by Andrist and Sehr (2nd ed., p. 153):
All algorithms use
std::swap()
andstd::move()
when moving elements around, but only if the move constructor and move assignment are marked noexcept. Therefore, it is important to have these implemented for heavy objects when using algorithms. If they are not available and exception free, the elements will be copied instead.
To break my question into pieces:
std::vector::push_back
, that run faster when fed with std::is_nothrow_move_constructible
types?noexcept
guideline?I know the third one might be a bit blurry. But if someone could come up with a simple example, this would be great.
Tagging our move constructor with "noexcept" tells the compiler that it will not throw any exceptions. This condition is checked in C++ using the type trait function: "std::is_no_throw_move_constructible". This function will tell you whether the specifier is correctly set on your move constructor.
Yes, throwing move constructors exist in the wild. Consider std::pair<T, U> where T is noexcept-movable, and U is only copyable (assume that copies can throw).
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.
vector push_back, resize, reserve, etc is very important case, as it is expected to be the most used container.
Anyway, take look at std::fuction
as well, I'd expect it to take advantage of noexcept move for small object optimization version.
That is, when functor object is small, and it has noexcept
move constructor, it can be stored in a small buffer in std::function
itself, not on heap. But if the functor doesn't have noexcept
move constructor, it has to be on heap (and don't move when std::function
is moved)
Overall, there ain't too many cases indeed.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With