3

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

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有一个不可复制的对象upiunique_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!
4

3 回答 3

4

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

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

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

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

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


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

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

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

于 2020-02-06T03:55:01.910 回答
2

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

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

因此 B 类也是可移动的。

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

有一个可复制但不可移动的类有实际的理由吗?几年前有人在这里问过这个问题。答案和评论很难找到任何实际理由想要一个可复制但不可移动的课程。

于 2020-02-06T05:36:26.480 回答
0

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

于 2020-02-06T02:50:21.967 回答