Search code examples
c++c++17stdvectorswapnoexcept

Why does std::vector's swap function have a different noexcept specification than all other container's swap functions?


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?


Solution

  • 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.