There were a couple of great answers about the copy-and-swap idiom, e.g., explaining the copy and swap idiom and explaining move semantics. The basic idiom working for both copy and move assignment looks like this:
T& T::operator=(T other) {
this->swap(other);
return *this;
}
This assignment works for both copy and move assignment because other
is copy or move constructed depending on whether the right hand side of the assignment is an lvalue or an rvalue.
Now let's have stateful allocators enter the picture: if T
is parameterized on an allocator type like, e.g., std::vector<S, A>
, the above idiom doesn't always work! Specifically, std::allocator_traits<A>
contains three types indicating whether the allocator should be propagated:
std::allocator_traits<A>::propagate_on_container_copy_assignment
std::allocator_traits<A>::propagate_on_container_move_assignment
std::allocator_traits<A>::propagate_on_container_swap
The default for these three traits is std::false_type
(see 20.6.8.1 [allocator.traits.types] paragraph 7, 8, and 9). The normal copy-and-swap idiom doesn't work if any of these traits is std::false_type
and the allocators are stateful with the possibility of comparing unequal. For the copy assignment the fix is fairly straight forward:
T& T::operator= (T const& other) {
T(other, this->get_allocator()).same_allocator_swap(*this);
return *this;
}
That is, first the object is copied supplying the allocator object of the LHS and then the members are swapped using a function which works if both objects use the same allocator, i.e., when other.get_allocator() == this->get_allocator()
.
When move assigning it is desirable not to copy the RHS if it can be moved instead. The RHS can be moved if the allocators are identical. Otherwise the object needs to be copied with the appropriate allocator, leading to an assignment operator like this
T& T::operator= (T&& other) {
T(std::move(other), this->get_allocator()).same_allocator_swap(*this);
return *this;
}
The approach here is to move construct a temporary while also passing an allocator. Doing so assumes that the type T
does have a "move constructor" taking both a T&&
for the object state and an allocator to specify the allocator to be used. The burden is put on the move constructor to copy or move according to the allocators being different or identical.
Since the first argument is passed differently the copy and the move assignment can't be folded into just one version of the assignment operator. As a result they need to take their arguments as references and need to explicitly copy or move the arguments inhibiting the potential for copy elision.
Is there a better approach to deal with the assignment operators when allocators are involved?
You seem to imply that the classic copy/swap idiom does work for the case that all of the propagate_on_
traits are not false. I don't believe this is the case. For example consider:
std::allocator_traits<A>::propagate_on_container_copy_assignment::value == true
std::allocator_traits<A>::propagate_on_container_swap::value == false
The classic copy/swap idiom assignment operator will not propagate the allocator from the rhs to the lhs, and will instead enter a state of undefined behavior if the two allocator states are not equal.
Your rewrite for the copy assignment operator also does not work this this combination of propagate_on_
traits, because it never propagates the allocator on copy assignment.
I do not believe the copy/swap idiom is advised if one wants to follow the rules for std::containers.
I keep an "allocator behavior" cheat sheet for myself that describes how these members should behave (in English as opposed to standard-eze).
copy assignment operator
If propagate_on_container_copy_assignment::value
is true, copy assigns the allocators. In this case, if allocators are not equal prior to the assignment, then first deallocates all memory on the lhs. Then proceeds with copy assigning the values, without transferring any memory ownership. I.e. this->assign(rhs.begin(), rhs.end())
.
move assignment operator
If propagate_on_container_move_assignment::value
is true, deallocate all memory on the lhs, move assign the allocators, then transfer memory ownership from the rhs to the lhs.
If propagate_on_container_move_assignment::value
is false, and the two allocators are equal, then deallocate all memory on the lhs, then transfer memory ownership from the rhs to the lhs.
If propagate_on_container_move_assignment::value
is false, and the two allocators are not equal, move assign as if this->assign(make_move_iterator(rhs.begin()), make_move_iterator(rhs.end())
.
These descriptions are intended to lead to the highest performance possible, while adhering to the C++11 rules for containers and allocators. Whenever possible, memory resources (such as vector capacity()
) are either transferred from the rhs, or reused on the lhs.
The copy/swap idiom always throws away memory resources (such as vector capacity()
) on the lhs, instead preemptively allocating such resources in a temporary just prior to deallocating them on the lhs.
For completeness:
swap
If propagate_on_container_swap::value
is true, swaps allocators. Regardless, swaps memory ownership. The behavior is undefined if propagate_on_container_swap::value
is false and the allocators are not equal.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With