我很惊讶这没有出现在我的搜索结果中,我以为有人会问这个问题,考虑到在 C + + 11中 move 语义的有用性:
(原因 其他比兼容性问题与现有的代码,即。)
简短的回答: 如果类型是可复制的,那么它也应该是可移动的。然而,相反的情况并非如此: 有些类型(如 std::unique_ptr)是可移动的,但复制它们没有意义; 这些类型自然是只能移动的类型。
std::unique_ptr
稍微长一点的回答接下来..。
有两种主要的类型(在其他更具特殊目的的类型中,如特征) :
类值类型,如 int或 vector<widget>。这些代表价值,自然应该是可复制的。在 C + + 11中,通常你应该把 move 看作是拷贝的一种优化,所以所有可拷贝的类型都应该是可移动的... ... 在通常情况下,move 只是一种有效的拷贝方式,你不再需要原始对象,无论如何都要销毁它。
int
vector<widget>
继承层次结构中存在的类引用类型,例如基类和具有虚成员函数或受保护成员函数的类。它们通常由指针或引用(通常是 base*或 base&)保存,因此不提供复制构造以避免切片; 如果您确实希望获得与现有对象类似的另一个对象,则通常调用类似于 clone的虚函数。它们不需要 move 构造或赋值,原因有二: 它们不可复制,而且它们已经有一个更有效的自然“ move”操作——你只需复制/移动指针到对象,对象本身根本不需要移动到一个新的内存位置。
base*
base&
clone
大多数类型属于这两种类型中的一种,但也有其他类型也很有用,只是比较少见。特别是在这里,表示资源的唯一所有权的类型,比如 std::unique_ptr,自然是仅移动类型,因为它们不像值(复制它们没有意义) ,但是你确实直接使用它们(不总是通过指针或引用) ,因此想要将这种类型的对象从一个地方移动到另一个地方。
Herb 的回答(在编辑之前)实际上给出了一个 不应该可移动的类型的很好的例子: std::mutex。
std::mutex
操作系统的本机互斥类型(例如 POSIX 平台上的 pthread_mutex_t)可能不是“位置不变的”,这意味着对象的地址是其值的一部分。例如,操作系统可能保留指向所有初始化互斥对象的指针列表。如果 std::mutex包含一个本机操作系统互斥类型作为数据成员,并且本机类型的地址必须保持固定(因为操作系统维护一个指向其互斥类型的指针列表) ,那么 std::mutex必须将本机互斥类型存储在堆上,这样当在 std::mutex对象之间移动时,它将保持在相同的位置,或者 std::mutex必须不移动。将它存储在堆上是不可能的,因为 std::mutex有一个 constexpr构造函数,并且必须有资格进行常量初始化(即静态初始化) ,这样才能保证在程序执行开始之前构造出一个全局 std::mutex,所以它的构造函数不能使用 new。所以剩下的唯一选择就是让 std::mutex变得不可移动。
pthread_mutex_t
constexpr
new
同样的推理也适用于包含需要固定地址的内容的其他类型。如果资源的地址必须保持固定,不要移动它!
对于不移动 std::mutex还有另外一个争论,那就是很难安全地做到这一点,因为你需要知道在互斥对象被移动的那一刻没有人试图锁定它。由于互斥锁是您可以用来防止数据竞争的构建块之一,因此如果它们本身不能安全地防止竞争,那将是非常不幸的!有了一个不可移动的 std::mutex,你知道任何人都可以做的唯一事情,一旦它已经建成,并在它已经被摧毁之前,是锁定和解锁它,这些操作是明确保证是线程安全的,而不是引入数据竞争。这个参数同样适用于 std::atomic<T>对象: 除非它们可以被原子移动,否则不可能安全地移动它们,另一个线程可能正在尝试在对象被移动的同时调用 compare_exchange_strong。因此,类型不应该可移动的另一种情况是,它们是安全并发代码的低级构建块,必须确保对它们的所有操作的原子性。如果对象值可能在任何时候被移动到一个新的对象,你需要使用一个原子变量来保护每一个原子变量,这样你就知道使用它是否安全或者它已经被移动了... 和一个原子变量来保护这个原子变量,等等..。
std::atomic<T>
compare_exchange_strong
我认为我可以概括地说,当一个对象只是一个纯粹的内存片段,而不是一个类型,作为一个值的持有者或值的抽象,移动它是没有意义的。像 int这样的基本类型不能移动: 移动它们只是一个副本。你不能从一个 int中提取内容,你可以复制它的值,然后将它设置为零,但它仍然是一个有值的 int,它只是一个字节的内存。但在语言术语中,int仍然是 可移动的,因为拷贝是一个有效的移动操作。然而,对于不可复制类型,如果你不想或者不能移动内存,你也不能复制它的值,那么它就是不可移动的。互斥量或原子变量是内存的一个特定位置(用特殊属性处理) ,所以移动没有意义,也不可复制,所以它是不可移动的。
实际上,当我四处搜索时,我发现 C + + 11中有相当多的类型是不可移动的:
mutex
recursive_mutex
timed_mutex
recursive_timed_mutex
condition_variable
type_info
error_category
locale::facet
random_device
seed_seq
ios_base
basic_istream<charT,traits>::sentry
basic_ostream<charT,traits>::sentry
atomic
once_flag
显然有一个关于 Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4的讨论
我找到的另一个原因是性能。 假设您有一个类“ a”,其中包含一个值。 您希望输出一个接口,该接口允许用户在有限的时间内(针对范围)更改值。
实现这一点的一种方法是从“ a”返回一个“作用域保护”对象,该对象在其析构函数中设置返回值,如下所示:
class a { int value = 0; public: struct change_value_guard { friend a; private: change_value_guard(a& owner, int value) : owner{ owner } { owner.value = value; } change_value_guard(change_value_guard&&) = delete; change_value_guard(const change_value_guard&) = delete; public: ~change_value_guard() { owner.value = 0; } private: a& owner; }; change_value_guard changeValue(int newValue) { return{ *this, newValue }; } }; int main() { a a; { auto guard = a.changeValue(2); } }
如果我让 change _ value _ Guard 可移动,我必须在它的析构函数中添加一个“如果”,以检查是否已经移动了该保护——这是一个额外的“如果”,并且会对性能产生影响。
是的,当然,它可能会被任何理智的优化器优化掉,但仍然很好的语言(这需要 C + + 17,但是,能够返回一个不可移动的类型,需要保证删除副本)不需要我们支付,如果我们不打算移动保护,而是从创建函数返回它(不支付,你不使用什么原则)。