129

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

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

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

4

4 回答 4

111

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

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

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

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

我想我会概括地说,当一个对象只是一块纯内存,而不是充当值持有者或值抽象的类型时,移动它是没有意义的。int不能移动的基本类型:移动它们只是一个副本。你不能从 a 中取出胆量int,你可以复制它的值,然后将其设置为零,但它仍然是int带有值的,它只是内存字节。但是anint仍然是可移动的在语言术语中,因为副本是有效的移动操作。然而,对于不可复制的类型,如果您不想或不能移动这块内存并且您也不能复制它的值,那么它就是不可移动的。互斥体或原子变量是内存的特定位置(使用特殊属性处理),因此移动没有意义,也不可复制,因此不可移动。

于 2013-01-13T14:52:52.540 回答
57

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

稍长的答案如下......

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

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

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

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

于 2013-01-13T11:47:41.800 回答
18

实际上,当我四处搜索时,我发现 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

于 2013-01-14T04:42:20.150 回答
1

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

于 2017-09-30T23:59:27.563 回答