提问人:user541686 提问时间:1/13/2013 最后编辑:Communityuser541686 更新时间:10/1/2017 访问量:13968
何时在 C++11 中使类型不可移动?
When to make a type non-movable in C++11?
问:
我很惊讶这没有出现在我的搜索结果中,考虑到 C++11 中移动语义的有用性,我认为有人以前会问这个问题:
我什么时候必须(或者对我来说是个好主意)在 C++11 中使类不可移动?
(也就是说,与现有代码的兼容性问题以外的原因。
答:
简短的回答:如果一个类型是可复制的,它也应该是可移动的。然而,反之则不然:有些类型是可移动的,但复制它们没有意义;这些自然是仅限移动的类型。std::unique_ptr
答案稍长一些......
有两种主要类型(以及其他更特殊用途的类型,例如特征):
类似值的类型,例如 或 .这些表示值,自然应该是可复制的。在 C++11 中,通常应该将 move 视为复制的优化,因此所有可复制类型都应该自然是可移动的......移动只是一种有效的复制方式,在通常常见的情况下,您不再需要原始对象,无论如何都会销毁它。
int
vector<widget>
存在于继承层次结构中的类似引用的类型,例如基类和具有虚拟或受保护成员函数的类。它们通常由指针或引用(通常是 或 )持有,因此不提供复制构造以避免切片;如果您确实想像获取现有对象一样获取另一个对象,则通常会调用类似 的虚拟函数。它们不需要移动构造或赋值,原因有两个:它们不可复制,并且它们已经具有更有效的自然“移动”操作 - 您只需复制/移动指向对象的指针,对象本身根本不需要移动到新的内存位置。
base*
base&
clone
大多数类型都属于这两类之一,但也有其他类型的类型也很有用,只是很少见。特别是在这里,表示资源唯一所有权的类型(如 )自然是仅移动类型,因为它们不具有类似值的类型(复制它们没有意义),但您确实直接使用它们(并不总是通过指针或引用),因此希望将这种类型的对象从一个地方移动到另一个地方。std::unique_ptr
评论
std::mutex
Herb 的回答(在它被编辑之前)实际上给出了一个不应该移动的类型的好例子:.std::mutex
操作系统的原生互斥类型(例如 在 POSIX 平台上)可能不是“位置不变”,这意味着对象的地址是其值的一部分。例如,OS 可能会保留指向所有已初始化互斥对象的指针列表。如果包含本机操作系统互斥类型作为数据成员,并且本机类型的地址必须保持固定(因为操作系统维护指向其互斥锁的指针列表),则必须将本机互斥类型存储在堆上,以便在对象之间移动时保持在同一位置,或者不得移动。将其存储在堆上是不可能的,因为 a 有一个构造函数,并且必须符合常量初始化(即静态初始化)的条件,以便保证在程序执行开始之前构造全局变量,因此其构造函数不能使用 .因此,剩下的唯一选择就是不可动摇。pthread_mutex_t
std::mutex
std::mutex
std::mutex
std::mutex
std::mutex
constexpr
std::mutex
new
std::mutex
同样的道理也适用于包含需要固定地址的内容的其他类型。如果资源的地址必须保持固定,请不要移动它!
不移动还有另一个论点,那就是很难安全地做到这一点,因为你需要知道在移动互斥锁的那一刻没有人试图锁定它。由于互斥锁是可用于防止数据争用的构建块之一,因此如果它们本身对争用不安全,那将是不幸的!对于不可移动的,你知道一旦它被构造出来,在它被摧毁之前,任何人唯一可以对它做的事情就是锁定它并解锁它,并且这些操作被明确保证是线程安全的,不会引入数据竞争。同样的论点也适用于对象:除非它们可以以原子方式移动,否则不可能安全地移动它们,否则另一个线程可能会在对象移动的那一刻尝试调用它。因此,类型不应可移动的另一种情况是,它们是安全并发代码的低级构建块,并且必须确保对它们的所有操作的原子性。如果对象值可能随时移动到新对象,则需要使用原子变量来保护每个原子变量,以便知道使用它是否安全或已移动...以及一个原子变量来保护该原子变量,等等......std::mutex
std::mutex
std::atomic<T>
compare_exchange_strong
我想我会概括地说,当一个对象只是一个纯粹的记忆片段,而不是一个充当值的持有者或值的抽象的类型时,移动它是没有意义的。基本类型,例如无法移动:移动它们只是一个副本。你不能把胆子从中扯出来,你可以复制它的值,然后将其设置为零,但它仍然是一个有值的,它只是内存的字节。但是在语言术语中,an 仍然是可移动的,因为副本是有效的移动操作。但是,对于不可复制的类型,如果您不想或无法移动内存片段,并且也无法复制其值,则它是不可移动的。互斥锁或原子变量是内存的特定位置(使用特殊属性处理),因此移动没有意义,也不可复制,因此不可移动。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
评论
iterators / iterator adaptors
同样是 std::time_point
s 和 std::d uration
s(也许还有其他我没有更彻底地检查/思考过)。std::move_iterator
std::reference_wrapper
也是如此。好吧,其他的似乎确实是不可移动的。
ios_base
type_info
facet
sentry
我发现的另一个原因 - 性能。 假设你有一个包含值的类“a”。 您希望输出一个接口,该接口允许用户在有限的时间内(对于范围)更改值。
实现此目的的一种方法是从“a”返回一个“scope guard”对象,该对象将值设置回其析构函数中,如下所示:
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可移动,我就必须在其析构函数中添加一个“if”,以检查保护是否已从中移出 - 这是一个额外的if,并且会影响性能。
是的,当然,它可能被任何理智的优化器优化掉,但仍然很高兴该语言(这需要 C++17,但要能够返回不可移动的类型需要保证复制省略)不需要我们支付,如果我们无论如何都不打算移动守卫,而不是从创建函数返回它(不要为不使用的东西付费原则)。
评论
+1
T x = std::move(anotherT);