Why aren’t container move assignment operators noexcept?

I believe we’re looking at a standards defect. The noexcept specification, if it is to be applied to the move assignment operator, is somewhat complicated. And I believe this statement to be true whether we are talking about basic_string or vector.

Based on [container.requirements.general]/p7 my English translation of what a container move assignment operator is supposed to do is:

C& operator=(C&& c)

If alloc_traits::propagate_on_container_move_assignment::value is
true, dumps resources, move assigns allocators, and transfers
resources from c.

If
alloc_traits::propagate_on_container_move_assignment::value is false
and get_allocator() == c.get_allocator(), dumps resources, and transfers
resources from c.

If
alloc_traits::propagate_on_container_move_assignment::value is false
and get_allocator() != c.get_allocator(), move assigns each c[i].

Notes:

  1. alloc_traits refers to allocator_traits<allocator_type>.

  2. When alloc_traits::propagate_on_container_move_assignment::value is true the move assignment operator can be specified noexcept because all it is going to is deallocate current resources and then pilfer resources from the source. Also in this case, the allocator must also be move assigned, and that move assignment must be noexcept for the container’s move assignment to be noexcept.

  3. When alloc_traits::propagate_on_container_move_assignment::value is false, and if the two allocators are equal, then it is going to do the same thing as #2. However one doesn’t know if the allocators are equal until run time, so you can’t base noexcept on this possibility.

  4. When alloc_traits::propagate_on_container_move_assignment::value is false, and if the two allocators are not equal, then one has to move assign each individual element. This may involve adding capacity or nodes to the target, and thus is intrinsically noexcept(false).

So in summary:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment::value &&
             is_nothrow_move_assignable<allocator_type>::value);

And I see no dependence on C::value_type in the above spec and so I believe it should apply equally well to std::basic_string despite C++11 specifying otherwise.

Update

In the comments below Columbo correctly points out that things have gradually been changing all the time. My comments above are relative to C++11.

For the draft C++17 (which seems stable at this point) things have changed somewhat:

  1. If alloc_traits::propagate_on_container_move_assignment::value is true, the spec now requires the move assignment of the allocator_type to not throw exceptions (17.6.3.5 [allocator.requirements]/p4). So one no longer needs to check is_nothrow_move_assignable<allocator_type>::value.

  2. alloc_traits::is_always_equal has been added. If this is true, then one can determine at compile time that point 3 above can not throw because resources can be transferred.

So the new noexcept spec for containers could be:

C& operator=(C&& c)
        noexcept(
             alloc_traits::propagate_on_container_move_assignment{} ||
             alloc_traits::is_always_equal{});

And, for std::allocator<T>, alloc_traits::propagate_on_container_move_assignment{} and alloc_traits::is_always_equal{} are both true.

Also now in the C++17 draft, both vector and string move assignment carry exactly this noexcept specification. However the other containers carry variations of this noexcept specification.

The safest thing to do if you care about this issue is to test explicit specializations of containers you care about. I’ve done exactly that for container<T> for VS, libstdc++ and libc++ here:

http://howardhinnant.github.io/container_summary.html

This survey is about a year old, but as far as I know is still valid.

Leave a Comment

Hata!: SQLSTATE[HY000] [1045] Access denied for user 'divattrend_liink'@'localhost' (using password: YES)