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


126

令我惊讶的是,它没有出现在我的搜索结果中,考虑到C ++ 11中移动语义的用处,我想有人会问过这个问题:

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

(原因比现有的代码,也就是兼容性问题。)


2
提升是永远领先一步- “昂贵的运动类型”(boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html
SChepurin

1
我认为这是一个很好且有用的问题(+1来自我),Herb(或似乎是他的双胞胎)给出了非常详尽的答案,因此我将其设为 FAQ条目。如果有人反对只是在休息室对我ping通,那么可以在那讨论。
2013年

1
AFAIK可移动类仍然可以进行切片,因此对于所有多态基类(即具有虚拟功能的所有基类)都禁止移动(和复制)。
菲利普

1
@Mehrdad:我只是说“ T具有移动构造函数”和“ T x = std::move(anotherT);合法”并不等效。后者是一个移动请求,在T没有移动ctor的情况下,它可能会落在复制ctor上。那么,“可移动”到底是什么意思?
sellibitze 2013年

1
@Mehrdad:查阅C ++标准库部分,了解“ MoveConstructible”的含义。某些迭代器可能没有move构造函数,但它仍然是MoveConstructible。注意“移动”人员所想到的各种定义。
sellibitze 2013年

Answers:


110

Herb的答案(在进行编辑之前)实际上给出了一个不应移动的类型的好例子:std::mutex

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

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

还有一个关于不移动的论点std::mutex,那就是安全地执行此操作非常困难,因为您需要知道在移动互斥对象时没有人试图锁定该互斥对象。由于互斥锁是您可以用来防止数据争用的构造块之一,因此,如果互斥量对抵御竞争本身并不安全,那将是不幸的!对于不动产,std::mutex您知道一旦构造它并且在销毁它之前,任何人都只能对它进行锁定和解锁,并且明确保证这些操作是线程安全的,并且不会引入数据争用。同样的论点也适用于std::atomic<T>对象:除非可以原子地移动它们,否则无法安全地移动它们,否则另一个线程可能正在尝试调用compare_exchange_strong现在正在移动物体。因此,类型不应移动的另一种情况是它们是安全并发代码的低级构建块,并且必须确保对它们进行的所有操作都是原子性的。如果可以随时将对象值移动到新对象,则需要使用原子变量来保护每个原子变量,以便知道使用它是安全的还是已经被移动了……以及要保护的原子变量该原子变量,依此类推...

我想我可以概括地说,当一个对象仅仅是一块纯内存,而不是充当值或值抽象的持有人的类型时,移动它就没有意义。基本类型(例如,int无法移动):移动它们只是一个副本。您无法从中删除胆量int,可以复制其值,然后将其设置为零,但是它仍然是int带值的,只是内存的字节数。但是一个int仍然可以移动用语言来表示,因为复制是有效的移动操作。但是,对于不可复制的类型,如果您不想或无法移动内存,并且也无法复制其值,则它是不可移动的。互斥锁或原子变量是内存的特定位置(使用特殊属性进行处理),因此移动没有任何意义,并且也是不可复制的,因此不可移动。


17
+1一个不太奇特的例子,该例子由于有特殊地址而无法移动,是有向图结构中的一个节点。
Potatoswatter 2013年

3
如果互斥锁是不可复制且不可移动的,我如何复制或移动包含互斥锁的对象?(就像一个线程安全类,它具有自己的互斥锁进行同步...)
tr3w 2013年

4
@ tr3w,除非您在堆上创建了互斥锁并通过unique_ptr或类似的方法持有它,否则您将无法使用
Jonathan Wakely 2013年

2
@ tr3w:除了互斥锁部分,您是否会移动整个类?
user541686 2013年

3
@BenVoigt,但是新对象将具有其自己的互斥体。我认为他的意思是具有用户定义的移动操作,该操作可以移动除互斥锁成员之外的所有成员。那么,如果旧对象即将到期怎么办?它的互斥锁与此一起到期。
Jonathan Wakely 2013年

57

简短答案:如果类型是可复制的,则它也应该是可移动的。但是,事实并非如此:有些类型std::unique_ptr是可移动的,但复制它们没有任何意义。这些自然是仅移动类型。

答案略长...

有两种主要类型(在其他一些更特殊用途的类型中,例如特征):

  1. 类似值的类型,例如intvector<widget>。这些代表值,自然应该是可复制的。在C ++ 11中,通常您应该将move视为对副本的优化,因此所有可复制类型自然应该是可移动的...在通常不使用的情况下,移动只是一种有效的复制方法不再需要原始物体,并且无论如何将要销毁它。

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

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


61
请问真正的香草萨特,请站起来吗?:)
fredoverflow

6
是的,我从使用一个OAuth Google帐户切换到另一个帐户,并且不费力地寻找一种将这两个登录信息合并的方法。(还有另一种反对OAuth的论点,其中还有更多令人信服的论点。)我可能不会再使用另一种论点了,因此,在偶尔的SO帖子中,我现在将使用这一论点。
Herb Sutter 2013年

7
我认为这std::mutex是不可移动的,因为POSIX互斥体由地址使用。
Puppy

9
@SChepurin:实际上,那就叫做HerbOverflow。
2013年

26
这引起了很多争议,没有人注意到它说什么时候类型应该是仅移动的,这不是问题吗?:)
Jonathan Wakely 2013年

18

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

  • 所有mutex类型(recursive_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 2013年

是的,我认为iterators / iterator adaptors应该将其删除,因为C ++ 11具有move_iterator?
Billz 2013年

好吧,我现在很困惑。您是在讨论移动目标的迭代器,还是在移动自身的迭代器?
user541686 2013年

1
也是如此std::reference_wrapper。好的,其他似乎确实是不可移动的。
Christian Rau

1
这些似乎分为三类:1.低级别并发相关类型(原子公司,互斥),2。多态基类(ios_basetype_infofacet),3。什锦奇怪的东西(sentry)。通常,程序员只能编写的唯一不可移动的类是第二类。
菲利普

0

我发现的另一个原因-性能。假设您有一个拥有价值的“ 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设置为可移动,则必须在其析构函数中添加一个“ if”,以检查后卫是否已移出-这是一个额外的if和性能影响。

是的,可以肯定,可以通过任何理智的优化器对其进行优化,但是仍然很高兴该语言(尽管这需要C ++ 17,才能返回不可移动的类型,但需要保证复制省略),不需要我们如果我们不打算从创建函数返回警卫(除了不付任何使用费的原则)之外退还警卫,那就要付出代价。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.