【问题标题】:When to make a type non-movable in C++11?何时在 C++11 中使类型不可移动?
【发布时间】:2012-12-27 11:47:41
【问题描述】:

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

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

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

【问题讨论】:

  • boost 总是领先一步——“昂贵的移动类型”(boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html)
  • @SChepurin:“搬家太贵”不是很清楚......我的意思是,如果某物对您的用例来说太贵了,那么您可能应该避免它,无论是在现实生活中还是在现实生活中编程或游戏中:-) 它没有告诉我任何我不知道的东西。
  • 我认为这是一个非常好的和有用的问题(+1 来自我),Herb(或他的双胞胎,as it seems)给出了非常彻底的回答,所以我将其作为常见问题解答条目。如果有人反对,请在the lounge ping 我,所以可以在那里讨论。
  • AFAIK 可移动类仍然可以进行切片,因此禁止所有多态基类(即所有具有虚函数的基类)的移动(和复制)是有意义的。
  • @Mehrdad:我只是说“T 有一个移动构造函数”和“T x = std::move(anotherT); 合法”是不等价的。后者是一个移动请求,如果 T 没有移动 ctor,它可能会退回到复制 ctor。那么,“可移动”究竟是什么意思?

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


【解决方案1】:

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

操作系统的本机互斥锁类型(例如,POSIX 平台上的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。因此,类型不应该是可移动的另一种情况是它们是安全并发代码的低级构建块,并且必须确保对它们的所有操作的原子性。如果对象值可能随时移动到新对象,则需要使用原子变量来保护每个原子变量,以便知道使用它是否安全或已被移动......以及要保护的原子变量那个原子变量,等等……

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

【讨论】:

  • +1 一个由于具有特殊地址而无法移动的不那么奇特的例子是有向图结构中的节点。
  • 如果互斥体不可复制且不可移动,我如何复制或移动包含互斥体的对象? (就像一个线程安全的类,它有自己的用于同步的互斥锁......)
  • @tr3w,你不能,除非你在堆上创建互斥锁并通过 unique_ptr 或类似的方式保存它
  • @tr3w: 你不会把整个类除了互斥部分吗?
  • @BenVoigt,但新对象将有自己的互斥锁。我认为他的意思是有用户定义的移动操作来移动除互斥锁成员之外的所有成员。那么如果旧对象过期了怎么办?它的互斥锁也随之过期。
【解决方案2】:

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

答案稍长...

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

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

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

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

【讨论】:

  • real Herb Sutter 站起来好吗? :)
  • 是的,我从使用一个 OAuth Google 帐户切换到另一个,并且懒得寻找一种方法来合并两个登录名。 (还有一个反对 OAuth 的论点,其中更有说服力。)我可能不会再使用另一个,所以我现在将在偶尔的 SO 帖子中使用这个。
  • 我认为std::mutex 是不可移动的,因为地址使用了 POSIX 互斥锁。
  • @SChepurin:其实那叫 HerbOverflow。
  • 这得到了很多支持,没有人注意到它说什么时候类型应该是只移动的,这不是问题吗? :)
【解决方案3】:

其实我搜了一下,发现很多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 应该被编辑掉,因为 C++11 有 move_iterator?
  • 我认为您将没有明确移动操作的对象(或不能比复制更有效地移动)误认为是不可移动类型。但最终所有可复制的类型也是可移动的。特别是所有迭代器都是可复制的,因此是可移动的,而不仅仅是std::move_iterator(无论如何,它的目的完全不同)。 std::time_points 和 std::durations 也是如此(也许还有其他我没有更彻底地检查/考虑过的人)。
  • std::reference_wrapper 也是如此。好的,其他的似乎确实是不可移动的。
  • 这些似乎分为三类:1.低级并发相关类型(原子,互斥体),2.多态基类(ios_basetype_infofacet), 3.各种奇怪的东西(sentry)。普通程序员编写的唯一不可移动的类可能属于第二类。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 2012-12-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-02
    • 2015-11-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多