Question

Does it make any sense to define operator< as noexcept?

I know that it makes perfect sense to define e.g. move constructor as noexcept (if possible) and I think I understand the effect of that.

But I have never seen a similar discussion about operator<() of a type used e.g. in std::set<>.

Does also non-throwing comparator have some (potential) optimizing effect (when used in std::set<>, std::map<>, std::sort() and similar)?

 7  146  7
1 Jan 1970

Solution

 11

A move constructor is somewhat special, because there is an obvious fallback. In a situation where you cannot allow a move to throw an exception you can call a noexcept move constructor and when the move constructor is not noexcept you can fallback to a copy. Hence, declaring the move constructor that does not throw exceptions as noexcept is a potential optimization.


For example std::vector::push_back does try to give a strong exception guarantee (from cppreference):

If an exception is thrown (which can be due to Allocator::allocate() or element copy/move constructor/assignment), this function has no effect (strong exception guarantee).

However, since C++11:

If T's move constructor is not noexcept and T is not CopyInsertable into *this, vector will use the throwing move constructor. If it throws, the guarantee is waived and the effects are unspecified.

Remember that pushing an element may require reallocations, ie to copy/move all elements to different memory location.

The above means: If the type is CopyInsertable, ie one has a choice between copying and moving, the vector will move them when the move constructor is noexcept. In this case you can gain performance by declaring the move constructor noexcept.

When the element type cannot be copied the move constuctor is used in any case (and the vector relaxes its exception guarantee, thanks to François Andrieux for making me aware of insert with similar balancing of exception safety vs performance). In this case what you gain by a noexcept move constructor is a stronger exception guarantee.


std::vector::push_back and similar examples is why there is so much discussion about declaring a move constructor noexcept.

For < there is no such obvious fallback that would be more expensive when < is not noexcept. Either you can call < or you can't.

Otherwise < has the same advantages as any other noexcept method, that the compiler knows it does never throw (and if it does std::terminate is called, but the usual stack unwinding does not necessarily take place).

2024-07-10
463035818_is_not_an_ai

Solution

 4

In addition to the other answer, let me note another optimization that is somewhat closer to noexcept comparator than to a noexcept move constructor.

Let's take a look at how unordered containers, like std::unordered_set, are implemented in libstdc++. These containers are node-based, and in pseudo code the internal node class definition looks like this:

template<class Key>
struct node {
   Key key;
  [size_t hash;]
};

Here the optional second member is present when is_fast_hash<Hash> && is_nothrow_invocable<Hash, Key> is false. (In real code this is implemented using template specialization).

is_fast_hash is there for performance reasons and is an internal type trait that checks whether a hasher is "fast" in some sense (fast for ints, slow for strings, etc.). A user-provided hash is fast by default.

The second type trait is needed because some operations must not throw exceptions. For example, std::unordered_set::erase() that takes an iterator must not throw, but it still has to compute hashes internally.

If is_nothrow_invocable<Hash, Key> is false, then node will be bigger by at least sizeof(std::size_t) bytes. By marking your hash function with noexcept you'll make node smaller, but at the same time your hasher might be invoked more often. Whether this will result in faster or slower code in practice is of course a matter of profiling.

2024-07-10
Evg