【问题标题】:Implicit move vs copy operations and containment隐式移动与复制操作和遏制
【发布时间】:2020-02-06 01:13:57
【问题描述】:

当一个类有一个未定义移动操作的成员时,我很难理解隐式移动操作:

int main() {
    struct A // no move: move = copy
    {
        A() = default;
        A(const A&) {
            cout << "A'copy-ctor\n";
        };
        A& operator=(const A&) {
            cout << "A'copy-assign\n";
            return *this;
        }
    };

    struct B
    {
        B() = default;
        A a; // does this make B non-moveable?
        unique_ptr<int> upi;
        // B(B&&) noexcept = default;
        // B& operator=(B&&)noexcept = default;
    };

    A a;
    A a2 = std::move(a); // ok use copy ctor instead of move one
    a2 = std::move(a); // ok use copy assignment instead of move one

    B b;
    B b2 = std::move(b); // why this works?
    b = std::move(b2); // and this works?
    // b = b2; // error: copy deleted because of non-copyable member upi

    cout << "\nDone!\n";
}

所以我看到A是一个不可移动的类,因为它的复制控制操作的定义所以它只能被复制并且任何试图移动这个类的对象的尝试,都使用相应的复制操作来代替.

到这里为止,如果我是正确的就可以了。但是B 有一个不可复制的对象upi,它是一个unique_ptr,因此复制操作被定义为删除的函数,所以我们不能复制这个类的对象。但是这个类有一个不可移动的对象a,因此我认为这个类(B)既不能复制也不能移动。但是为什么b2 的初始化和b 的赋值工作正常呢?究竟发生了什么?

B b2 = std::move(b); // ok?!

为什么上面的行调用了类A的复制构造函数,它调用了B的移动构造函数?

  • 对我来说情况变得更糟:如果我取消注释 B 中的移动操作行,上面的初始化将无法编译并抱怨引用已删除的函数,赋值也是如此!

谁能帮我看看到底发生了什么?在将问题发布到此处之前,我已经在 cppreference 和许多网站上进行了谷歌搜索和阅读。

输出:

A'copy-ctor
A'copy-assign
A'copy-ctor
A'copy-assign

Done!

【问题讨论】:

  • 通常,复制构造函数或复制赋值运算符采用T const&amp;,它可以绑定到所有表达式(左值和右值)。所以这是一个可行的候选函数,如果没有竞争的移动函数,它将默认赢得重载决议

标签: c++ c++11 copy-constructor move-constructor implicit-methods


【解决方案1】:

请记住在 C++ 中“移动”数据的含义(假设我们遵循通常的约定)。如果您将对象x 移动到对象y,那么y 会收到x 中的所有数据,而x 是……好吧,我们不在乎x 是什么,只要它仍然对销毁有效。我们通常认为x 丢失了所有数据,但这不是必需的。所需要的只是x 是有效的。如果x 最终得到与y 相同的数据,我们不在乎。

x 复制到y 会导致y 接收x 中的所有数据,并且x 处于有效状态(假设复制操作遵循约定并且没有错误)。因此,复制算作移动。除了复制操作之外还定义移动操作的原因不是为了允许新的东西,而是为了在某些情况下允许更高的效率。除非您采取措施防止移动,否则可以移动任何可以复制的东西。

所以我看到A 是一个不可移动的类,因为它定义了复制控制操作,所以它只能被复制,并且任何试图移动这个类的对象的尝试,都会使用相应的复制操作来代替.

我看到的是 A 是一个 moveable 类(尽管缺少移动构造函数和移动赋值),因为它定义了复制控制操作。任何移动此类对象的尝试都将依赖于相应的复制操作。如果您希望类可复制但不可移动,则需要删除移动操作,同时保留复制操作。 (试试看。将A(A&amp;&amp;) = delete; 添加到A 的定义中。)

B 类有一个可以移动或复制的成员,以及一个可以移动但不能复制的成员。所以B 本身可以移动但不能复制。当B 被移动时,unique_ptr 成员将按预期移动,A 成员将被复制(A 类型移动对象的后备)。


事情变得更糟:如果我取消注释 B 中的移动操作行,上面的初始化将不会编译抱怨引用已删除的函数,赋值也是如此!

更仔细地阅读错误消息。当我复制这个结果时,“使用已删除函数”错误后面跟着一个提供更多详细信息的注释:移动构造函数被删除,因为“它的异常规范与隐式异常规范不匹配”。删除 noexcept 关键字允许代码编译(使用 gcc 9.2 和 6.1)。

或者,您可以将noexcept 添加到A 的复制构造函数和复制赋值中(在B 的移动操作中保留noexcept)。这是证明B 的默认移动操作使用A 的复制操作的一种方式。

【讨论】:

  • 非常感谢!非常惊人的解释。
【解决方案2】:

以下是@JaMiT 出色答案的摘要:

A 类通过它的复制构造函数和复制赋值运算符可移动,即使 A 类不是 MoveConstructible 也不是 MoveAssignable。请参阅 cppreference.com 页面上关于 MoveConstructibleMoveAssignable 的注释。

因此 B 类也是可移动的。

该语言允许您通过显式 =删除移动构造函数和移动赋值来防止 A 类的可移动性,即使 A 类仍然是可复制的。

有一个可复制但不可移动的类有实际的理由吗?几年前有人问过这个问题here。答案和 cmets 努力寻找任何实际理由想要一个可复制但不可移动的类。

【讨论】:

    【解决方案3】:

    std::move 不强制复制对象。它只返回 &&-reference(它允许编译器使用移动 ctor/assign 运算符)。
    如果复制了 1,2 对象。
    在 3,4 情况下(我认为)对象被移动。但是 A 仍然被复制,因为它不能被移动。

    【讨论】:

      猜你喜欢
      • 2015-11-09
      • 2011-11-19
      • 1970-01-01
      • 1970-01-01
      • 2015-10-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-29
      相关资源
      最近更新 更多