Why aren't container move assignment operators noexcept?
Asked Answered
O

3

46

I noticed that std::string's (really std::basic_string's) move assignment operator is noexcept. That makes sense to me. But then I noticed that none of the standard containers (e.g., std::vector, std::deque, std::list, std::map) declares its move assignment operator noexcept. That makes less sense to me. A std::vector, for example, is typically implemented as three pointers, and pointers can certainly be move-assigned without throwing an exception. Then I thought that maybe the problem is with moving the container's allocator, but std::string's have allocators, too, so if that were the issue, I'd expect it to affect std::string.

So why is std::string's move assignment operator noexcept, yet the move assignment operators for the standard containers are not?

Ozzy answered 8/9, 2012 at 17:7 Comment(6)
Where exactly are you seeing this?Hooded
@LokiAstari in C++ standard i think.Countersign
@Mr.Anubis: Yes. The OP is asking why it isn't noexcept.Reactant
@NicolBolas idiot me :) , I took the question in reverseHungry
open-std.org/JTC1/SC22/WG21/docs/lwg-active.html#2063 about operator = for string&&.Countersign
@ForEveR: Thanks for the pointer to that open issue. It sheds a lot of light on the matter.Ozzy
O
26

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.

Oar answered 8/9, 2012 at 22:15 Comment(3)
Why should it apply to basic_string?Reactant
@NicolBolas: [container.requirements.general]/p13: All of the containers defined in this Clause and in (21.4) except array meet the additional requirements of an allocator-aware container, as described in Table 99.Oar
The condition can be improved by employing if_always_equal (C++17). Btw., you probably wanna use the allocator trait specialization and not the raw allocator type.Monti
R
9

I think the reasoning for this goes like this.

basic_string only works with non-array POD types. As such, their destructors must be trivial. This means that if you do a swap for move-assignment, it doesn't matter as much to you that the original contents of the moved-to string haven't been destroyed yet.

Whereas containers (basic_string is not technically a container by the C++ spec) can contain arbitrary types. Types with destructors, or types that contain objects with destructors. This means that it is more important to the user to maintain control over exactly when an object is destroyed. It specifically states that:

All existing elements of a [the moved-to object] are either move assigned to or destroyed.

So the difference does make sense. You can't make move-assignment noexcept once you start deallocating memory (through the allocator) because that can fail via exception. Thus, once you start requiring that memory is deallocated on move-assign, you give up being able to enforce noexcept.

Reactant answered 8/9, 2012 at 17:33 Comment(4)
basic_string is not container by standard. And operator = for basic_string is really noexcept. Effects: If *this and str are not the same object, modifies *this as shown in Table 71. [ Note: A valid implementation is swap(str). —end note ]Countersign
Deallocation should never fail. If releasing a resource can fail with an exception, it's virtually impossible to write fault-tolerant C++ code. Deallocation also happens in the container's own destructor (which will also call the user-defined type's destructors), but destructors in C++11 are noexcept by default, and for good reason.Retentive
@AdamH.Peterson: Destructors are noexcept by default, but allocators are not. Notably, std::allocator::deallocate isn't noexcept.Reactant
@NicolBolas, that may be true, but the stdandard library's containers' destructors are noexcept and those destructors invoke deallocations on allocators. If the allocator's deallocation function fails, you will eventually see a program abort from a noexcept violation. That suggests to me that the reason a move() might not be noexcept has nothing to do with deallocation failure.Retentive
P
0

The move assignment operator in container classes is defined to be noexcept because many containers are designed to implement the strong exception safety guarantee. Containers implement the strong exception safety guarantee because back before there were move assignment operators, the container had to be copied. If anything went wrong with the copy, the new storage was deleted and the container remained unchanged. Now we're stuck with that behavior. If the move assignment op is not noexcept, the slower copy assignment operator is invoked instead.

Possessive answered 8/1, 2018 at 2:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.