I noticed that the std::vector
container's swap function has a different noexcept-specification than all other containers. Specifically, the function is noexcept if the expression
std::allocator_traits<Allocator>::propagate_on_container_swap || std::allocator_traits<Allocator>::is_always_equal
is true, but other containers require the expression std::allocator_traits<Allocator>::is_always_equal
to be true.
Since the behavior of the swap functions is the same, why is the noexcept-specification different in just the std::vector
container?
The noexcept
specifications discussed in your question were introduced in C++17 through N4258. This paper differentiated vector
/string
from other containers (deque
, list
, map
, etc.) in terms of noexcept
specifications for move assignment1 and the swap
function, summarized below2:
Container | Move assignment | swap |
---|---|---|
vector /string |
POCMA || IAE |
POCS || IAE |
Others | IAE |
IAE |
For containers aside from vector
and string
, relying solely on POCMA
for the noexcept
guarantee concerning move assignment is insufficient. Some implementations require allocating a sentry with the moved allocator to achieve the moved-from state, which could potentially throw. Notably, Microsoft's implementation of std::set
has a throwing move assignment operator. The paper explains:
[...] even with POCMA we can’t guarantee
noexcept
if the container is “kind of” node based (deque, lists, associative, unordered): The moved-from object needs to reallocate if the allocator propagated and is not always equal (because I can't steal the LHS' memory).
Now, concerning swap
, the aforementioned reasoning does not apply since we only need to exchange everything, including the mentioned sentry object. Therefore, no reallocation is necessary, and POCS || IAE
suffices. A similar rationale is provided in P0177R2:
[...] This should not be an issue for swap operations though, as allocators are expected to be exchanged, along with two data structures that satisfy the invariants. [...]
Having discussed these points, it appears there may be a flaw in the current standard. One possible reason could be the committee's intent to allow node-based containers' swap
operations to be implemented based on their move assignments, leading to the same noexcept
specification as the latter. Unfortunately, the discussion mentioned in N4258, which might provide further insights, is non-public (from WG21 Wiki), so this remains speculative.
For additional insights, I investigated the implementations of node-based containers across the three major vendors to ascertain the noexcept
guarantees they offer for swap
functions. Here are the findings3:
Container | libc++ | libstdc++ | Microsoft STL |
---|---|---|---|
deque |
noexcept |
noexcept |
noexcept |
list |
noexcept |
noexcept |
noexcept |
forward_list |
noexcept |
noexcept |
noexcept |
set |
noexcept |
noexcept |
noexcept |
unordered_set |
noexcept |
noexcept |
noexcept |
As depicted in the table, all these implementations indeed provide reinforced noexcept
specifications for their respective swap
functions.
1Although move assignment isn't directly addressed in the question, it's mentioned here because it could influence the design of noexcept
specifications for swap
operations.
2POCMA
stands for propagate_on_container_move_assignment
, POCS
stands for propagate_on_container_swap
, and IAE
stands for is_always_equal
. Predicates such as Compare
specifications are omitted here for brevity.
3Similar to the earlier footnote, predicate specifications have been omitted for brevity.