但是,在现实世界中是否存在移动分配、
move-construct(或swap)实际上可能会抛出?
是的。考虑std::list 的实现。 end 迭代器必须指向列表中“最后一个元素之后的位置”。存在std::list 的实现,其中end 指向的是一个动态分配的节点。甚至默认构造函数也分配了这样一个节点,这样当你调用end()时,就有了指向的东西。
在这样的实现中,每个构造函数必须为end()分配一个节点以指向……甚至是移动构造函数。该分配可能会失败,并引发异常。
同样的行为可以扩展到任何基于节点的容器。
这些基于节点的容器也有实现“短字符串”优化:它们将结束节点嵌入到容器类本身中,而不是动态分配。因此默认构造函数(和移动构造函数)不需要分配任何东西。
如果容器的分配器propagate_on_container_move_assignment::value 为假,并且lhs 中的分配器不等于rhs 中的分配器,则移动赋值运算符可以抛出任何container<X>。在这种情况下,禁止移动赋值运算符将内存所有权从 rhs 转移到 lhs。如果您使用std::allocator,则不会发生这种情况,因为std::allocator 的所有实例彼此相等。
更新
以下是propagate_on_container_move_assignment::value 为假时的一致且可移植的示例。它已经针对最新版本的 VS、gcc 和 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';
}
}
这个程序输出:
1
这表明vector<T, A> 移动赋值运算符在propagate_on_container_move_assignment::value 为假且两个分配器比较不相等时复制/移动其元素。如果这些复制/移动中的任何一个抛出,则容器移动分配抛出。