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

1

25

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?

Shadowy answered 10/5, 2023 at 19:44 Comment(7)
Howard Hinnant apparently answered your question in this comment!Rato
The comment explains why the swap function's noexcept-specification was implemented that way, but it does not answer the question "why is the noexcept-specification different just in the std::vector container".Shadowy
Unfortunately I could only find links to non-public discussion on this topic. Maybe it was just caution when they introduced the conditional noexcept specifiers for both swap and move operations. It is much easier to verify that it won't cause implementation problems for vector than for the other containers and vector should also be higher priority. The standard library implementation is allowed to use stricter noexcept specification than specified in the standard, so that's just the minimum that all implementations should agree upon.Allinclusive
@user17732522, you may be right. However, if the effects and the behavior of the swap function is the same in all containers, I think the noexcept-specification should be the same to be consistent and guarantee a stricter minimum specification in all implementations. Does it have sense?Shadowy
The history of this topic is certainly interesting. The noexcept(false) situation can only arise here when using custom, stateful allocators, since the std::allocator template is stateless and therefore always equal. Writing such an allocator is something most C++ programmers will not need to do, but apparently Bloomberg had a major codebase using these. Consequently, Bloomberg developers have provided most of the input I found on allocators, swapping and noexcept. The "Lakos rule" on the use of noexcept in the standard library is named after one of them.Rato
After reading this, it would not surprise me if they were simply using those allocators primarily or exclusively with vectors, and therefore were most interested in making this particular change to the standard.Rato
The post says "other containers require only". This is highly misleading, because it sounds like the "others" have a less strict requirement. But this is the opposite! "Others" require IAE (which is strict), but vector requires POCS || IAE, which is a much looser requirement.Peipus
M
1

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.

Malachy answered 12/4 at 10:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.