However, is there any real-world case where a move-assign,
move-construct (or swap) might actually throw?
Yes. Consider an implementation of std::list
. The end
iterator must point "one past the last element" in the list. There exist implementations of std::list
where what end
points to is a dynamically allocated node. Even the default constructor allocates such a node so that when you call end()
, there is something to point to.
In such an implementation, every constructor must allocate a node for end()
to point to… even the move constructor. That allocation may fail, and throw an exception.
This same behavior can extend to any node-based container.
There are also implementations of these node-based containers that do a "short-string" optimization: They embed the end node within the container class itself, instead of dynamically allocating. Thus the default constructor (and move constructor) need not allocate anything.
The move assignment operator can throw for any container<X>
if for the container's allocator propagate_on_container_move_assignment::value
is false, and if the allocator in the lhs is not equal to the allocator in the rhs. In that case the move assignment operator is forbidden from transferring memory ownership from the rhs to the lhs. This can not happen if you are using std::allocator
, as all instances of std::allocator
are equal to one another.
Update
Here is a conforming and portable example of the case when propagate_on_container_move_assignment::value
is false. It has been tested against the latest version of VS, gcc and clang.
#include <cassert>
#include <cstddef>
#include <iostream>
#include <vector>
template <class T>
class allocator
{
int id_;
public:
using value_type = T;
allocator(int id) noexcept : id_(id) {}
template <class U> allocator(allocator<U> const& u) noexcept : id_(u.id_) {}
value_type*
allocate(std::size_t n)
{
return static_cast<value_type*>(::operator new (n*sizeof(value_type)));
}
void
deallocate(value_type* p, std::size_t) noexcept
{
::operator delete(p);
}
template <class U, class V>
friend
bool
operator==(allocator<U> const& x, allocator<V> const& y) noexcept
{
return x.id_ == y.id_;
}
};
template <class T, class U>
bool
operator!=(allocator<T> const& x, allocator<U> const& y) noexcept
{
return !(x == y);
}
template <class T> using vector = std::vector<T, allocator<T>>;
struct A
{
static bool time_to_throw;
A() = default;
A(const A&) {if (time_to_throw) throw 1;}
A& operator=(const A&) {if (time_to_throw) throw 1; return *this;}
};
bool A::time_to_throw = false;
int
main()
{
vector<A> v1(5, A{}, allocator<A>{1});
vector<A> v2(allocator<A>{2});
v2 = std::move(v1);
try
{
A::time_to_throw = true;
v1 = std::move(v2);
assert(false);
}
catch (int i)
{
std::cout << i << '\n';
}
}
This program outputs:
1
which indicates that the vector<T, A>
move assignment operator is copy/moving its elements when propagate_on_container_move_assignment::value
is false and the two allocators in question do not compare equal. If any of those copies/moves throws, then the container move assignment throws.