什么时候在C ++ 11中制作一个不可移动的类型?

时间:2013-01-13 11:05:06

标签: c++ c++11 move-semantics c++-faq

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

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

(原因其他与现有代码的兼容性问题,即。)

4 个答案:

答案 0 :(得分:107)

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

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

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

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

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

答案 1 :(得分:57)

简答:如果某个类型是可复制的,那么它也应该是可移动的。然而,相反的情况并非如此:像std::unique_ptr这样的某些类型是可移动的,但复制它们没有意义;这些只是自然移动类型。

接下来的答案稍长......

有两种主要类型(其他更特殊的类型,如特征):

  1. 类似于价值的类型,例如intvector<widget>。这些代表值,自然应该是可复制的。在C ++ 11中,通常你应该将move视为复制的优化,因此所有可复制类型都应该是可移动的...移动只是一种有效的方式来复制常见的情况,你不应该无论如何都需要原始物体,无论如何都要毁掉它。

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

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

答案 2 :(得分:17)

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

答案 3 :(得分:0)

我发现的另一个原因 - 表现。 假设你有一个持有一个值的类'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,但能够返回一个不可移动的类型需要保证复制省略)如果我们不打算移动警卫,除了从创建功能返回它之前不要求我们支付费用(不要付钱给你不用的原则)。