何时在 C++11 中使类型不可移动?

When to make a type non-movable in C++11?

提问人:user541686 提问时间:1/13/2013 最后编辑:Communityuser541686 更新时间:10/1/2017 访问量:13968

问:

我很惊讶这没有出现在我的搜索结果中,考虑到 C++11 中移动语义的有用性,我认为有人以前会问这个问题:

我什么时候必须(或者对我来说是个好主意)在 C++11 中使类不可移动?

(也就是说,与现有代码的兼容性问题以外的原因。

C ++11 移动语义 C++-FAQ

评论

3赞 SChepurin 1/13/2013
Boost 总是领先一步——“移动类型成本高昂”(boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html)
1赞 user541686 1/13/2013
@SChepurin:“搬家成本高”不是很清楚......我的意思是,如果某些东西对你的用例来说太贵了,那么你可能应该避免它,无论是在现实生活中,还是在编程或游戏中:-)它没有告诉我任何我不知道的事情。
1赞 sbi 1/13/2013
我认为这是一个非常好和有用的问题(来自我),Herb(或他的双胞胎,看起来)给出了非常彻底的答案,所以我把它作为一个FAQ条目。如果有人反对,只需在休息室联系我,所以可以在那里讨论。+1
2赞 Philipp 1/14/2013
AFAIK 可移动类仍然可以进行切片,因此禁止移动(和复制)所有多态基类(即所有具有虚拟函数的基类)是有意义的。
1赞 sellibitze 1/14/2013
@Mehrdad:我只是说“T 有一个移动构造函数”和“合法”并不等同。后者是一个移动请求,如果 T 没有移动 ctor,它可能会回退到复制 ctor 上。那么,“可移动”到底是什么意思呢?T x = std::move(anotherT);

答:

58赞 Herb Sutter 1/13/2013 #1

简短的回答:如果一个类型是可复制的,它也应该是可移动的。然而,反之则不然:有些类型是可移动的,但复制它们没有意义;这些自然是仅限移动的类型。std::unique_ptr

答案稍长一些......

有两种主要类型(以及其他更特殊用途的类型,例如特征):

  1. 类似值的类型,例如 或 .这些表示值,自然应该是可复制的。在 C++11 中,通常应该将 move 视为复制的优化,因此所有可复制类型都应该自然是可移动的......移动只是一种有效的复制方式,在通常常见的情况下,您不再需要原始对象,无论如何都会销毁它。intvector<widget>

  2. 存在于继承层次结构中的类似引用的类型,例如基类和具有虚拟或受保护成员函数的类。它们通常由指针或引用(通常是 或 )持有,因此不提供复制构造以避免切片;如果您确实想像获取现有对象一样获取另一个对象,则通常会调用类似 的虚拟函数。它们不需要移动构造或赋值,原因有两个:它们不可复制,并且它们已经具有更有效的自然“移动”操作 - 您只需复制/移动指向对象的指针,对象本身根本不需要移动到新的内存位置。base*base&clone

大多数类型都属于这两类之一,但也有其他类型的类型也很有用,只是很少见。特别是在这里,表示资源唯一所有权的类型(如 )自然是仅移动类型,因为它们不具有类似值的类型(复制它们没有意义),但您确实直接使用它们(并不总是通过指针或引用),因此希望将这种类型的对象从一个地方移动到另一个地方。std::unique_ptr

评论

62赞 fredoverflow 1/13/2013
请真正的赫伯·萨特站起来吗?:)
7赞 Herb Sutter 1/13/2013
是的,我从使用一个 OAuth Google 帐户切换到另一个帐户,并且懒得寻找一种方法来合并给我的两个登录名。(另一个反对OAuth的论点,其中有更令人信服的论点。我可能不会再使用另一个,所以这是我现在偶尔用于 SO 帖子的内容。
7赞 Puppy 1/13/2013
我认为这是不可动摇的,因为 POSIX 互斥锁是由地址使用的。std::mutex
9赞 sbi 1/13/2013
@SChepurin:实际上,这叫做 HerbOverflow。
29赞 Jonathan Wakely 1/13/2013
这得到了很多赞成,没有人注意到它说什么时候一个类型应该是只移动的,这不是问题吗?:)
114赞 Jonathan Wakely 1/13/2013 #2

Herb 的回答(在它被编辑之前)实际上给出了一个不应该移动的类型的好例子:.std::mutex

操作系统的原生互斥类型(例如 在 POSIX 平台上)可能不是“位置不变”,这意味着对象的地址是其值的一部分。例如,OS 可能会保留指向所有已初始化互斥对象的指针列表。如果包含本机操作系统互斥类型作为数据成员,并且本机类型的地址必须保持固定(因为操作系统维护指向其互斥锁的指针列表),则必须将本机互斥类型存储在堆上,以便在对象之间移动时保持在同一位置,或者不得移动。将其存储在堆上是不可能的,因为 a 有一个构造函数,并且必须符合常量初始化(即静态初始化)的条件,以便保证在程序执行开始之前构造全局变量,因此其构造函数不能使用 .因此,剩下的唯一选择就是不可动摇。pthread_mutex_tstd::mutexstd::mutexstd::mutexstd::mutexstd::mutexconstexprstd::mutexnewstd::mutex

同样的道理也适用于包含需要固定地址的内容的其他类型。如果资源的地址必须保持固定,请不要移动它!

不移动还有另一个论点,那就是很难安全地做到这一点,因为你需要知道在移动互斥锁的那一刻没有人试图锁定它。由于互斥锁是可用于防止数据争用的构建块之一,因此如果它们本身对争用不安全,那将是不幸的!对于不可移动的,你知道一旦它被构造出来,在它被摧毁之前,任何人唯一可以对它做的事情就是锁定它并解锁它,并且这些操作被明确保证是线程安全的,不会引入数据竞争。同样的论点也适用于对象:除非它们可以以原子方式移动,否则不可能安全地移动它们,否则另一个线程可能会在对象移动的那一刻尝试调用它。因此,类型不应可移动的另一种情况是,它们是安全并发代码的低级构建块,并且必须确保对它们的所有操作的原子性。如果对象值可能随时移动到新对象,则需要使用原子变量来保护每个原子变量,以便知道使用它是否安全或已移动...以及一个原子变量来保护该原子变量,等等......std::mutexstd::mutexstd::atomic<T>compare_exchange_strong

我想我会概括地说,当一个对象只是一个纯粹的记忆片段,而不是一个充当值的持有者或值的抽象的类型时,移动它是没有意义的。基本类型,例如无法移动:移动它们只是一个副本。你不能把胆子从中扯出来,你可以复制它的值,然后将其设置为零,但它仍然是一个有值的,它只是内存的字节。但是在语言术语中,an 仍然是可移动的,因为副本是有效的移动操作。但是,对于不可复制的类型,如果您不想或无法移动内存片段,并且也无法复制其值,则它是不可移动的。互斥锁或原子变量是内存的特定位置(使用特殊属性处理),因此移动没有意义,也不可复制,因此不可移动。intintintint

评论

18赞 Potatoswatter 1/13/2013
+1 一个不那么奇特的例子是有向图结构中的一个节点,因为它有一个特殊的地址而无法移动。
3赞 tr3w 1/14/2013
如果互斥锁不可复制且不可移动,如何复制或移动包含互斥锁的对象?(就像一个线程安全类,它有自己的互斥锁用于同步......
4赞 Jonathan Wakely 1/14/2013
@tr3w,你不能,除非你在堆上创建互斥锁并通过unique_ptr或类似方式保存它
2赞 user541686 1/14/2013
@tr3w:除了互斥锁部分之外,你不会移动整个类吗?
3赞 Jonathan Wakely 1/14/2013
@BenVoigt,但新对象将有自己的互斥锁。我认为他的意思是具有用户定义的移动操作,这些操作可以移动除互斥成员之外的所有成员。那么,如果旧对象即将过期怎么办?它的互斥锁也随之过期。
22赞 billz 1/14/2013 #3

实际上,当我四处搜索时,我发现 C++11 中的一些类型是不可移动的:

  • 所有类型( , , ,mutexrecursive_mutextimed_mutexrecursive_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

评论

1赞 user541686 1/14/2013
...迭代器不应该是可移动的?!什么。。。为什么?
0赞 billz 1/14/2013
是的,我认为应该像 C++11 move_iterator一样编辑掉?iterators / iterator adaptors
1赞 Christian Rau 1/14/2013
我认为您将没有显式移动操作的对象(或者移动效率高于复制)的对象误认为是不可移动的类型。但最终,所有可复制的类型也是可移动的。特别是所有的迭代器都是可复制的,因此是可移动的,而不仅仅是(无论如何,它有一个完全不同的目的)。同样是 std::time_points 和 std::d urations(也许还有其他我没有更彻底地检查/思考过)。std::move_iterator
1赞 Christian Rau 1/14/2013
std::reference_wrapper也是如此。好吧,其他的似乎确实是不可移动的。
1赞 Philipp 1/15/2013
这些似乎分为三类:1.与低级并发相关的类型(原子、互斥锁),2.。多态基类 (, , ), 3.各种奇怪的东西()。普通程序员唯一会编写的不可移动类可能属于第二类。ios_basetype_infofacetsentry
1赞 saarraz1 10/1/2017 #4

我发现的另一个原因 - 性能。 假设你有一个包含值的类“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,但要能够返回不可移动的类型需要保证复制省略)不需要我们支付,如果我们无论如何都不打算移动守卫,而不是从创建函数返回它(不要为不使用的东西付费原则)。