在可移动和不可复制的类上使用移动和交换习语是不是有意义

Posted

技术标签:

【中文标题】在可移动和不可复制的类上使用移动和交换习语是不是有意义【英文标题】:Does it make sense to use the move-and-swap idiom on a movable and non-copyable class在可移动和不可复制的类上使用移动和交换习语是否有意义 【发布时间】:2011-10-13 00:50:27 【问题描述】:

如果我有这样的课程

class Foo
public:
    Foo()...
    Foo(Foo && rhs)...
    operator=(Foo rhs) swap(*this, rhs);
    void swap(Foo &rhs);
private:
    Foo(const Foo&);
// snip: swap code
;
void swap(Foo& lhs, Foo& rhs);

如果我没有复制构造函数,按值实现 operator= 并交换是否有意义?它应该防止复制我的 Foo 类的对象,但允许移动。

这个类是不可复制的,所以我应该不能复制构造或复制分配它。

编辑

我已经用这个测试了我的代码,它似乎有我想要的行为。

#include <utility>
#include <cstdlib>
using std::swap;
using std::move;
class Foo
public: Foo():a(rand()),b(rand()) 
        Foo(Foo && rhs):a(rhs.a), b(rhs.b)rhs.a=rhs.b=-1;
        Foo& operator=(Foo rhs)swap(*this,rhs);return *this;
        friend void swap(Foo& lhs, Foo& rhs)swap(lhs.a,rhs.a);swap(lhs.b,rhs.b);
private:
    //My compiler doesn't yet implement deleted constructor
    Foo(const Foo&);
private:
    int a, b;
;

Foo make_foo()

    //This is potentially much more complicated
    return Foo();


int main(int, char*[])

    Foo f1;
    Foo f2 = make_foo(); //move-construct
    f1 = make_foo(); //move-assign
    f2 = move(f1);
    Foo f3(move(f2));
    f2 = f3; // fails, can't copy-assign, this is wanted
    Foo f4(f3); // fails can't copy-construct

    return 0;

【问题讨论】:

我想,如果你已经有了交换代码,这不会有什么坏处。您不能通过分配意外复制,因为分配按值获取参数,这仅适用于您的情况。另外,说Foo(const Foo&amp;) = delete; “移动和交换”,当然。 @Kerrek SB 不幸的是,我当前工作的编译器不支持 = default 也不支持 =delete。见connect.microsoft.com/VisualStudio/feedback/details/679447/… 这是一件可怕的事情。没有复制 ctor -> 没有复制分配。 做正确的事并提供移动任务Foo&amp; operator=(Foo&amp;&amp;) 【参考方案1】:

移动和交换确实是合理的。如果禁用复制构造函数,则调用此函数的唯一方法是使用移动构造函数构造参数。这意味着如果你写

lhs = rhs; // Assume rhs is an rvalue

然后operator =的参数的构造函数将使用移动构造函数初始化,清空rhs并将参数设置为rhs的旧值。对swap 的调用然后交换lhs 的旧值和rhs 的旧值,留下lhs 持有rhs 的旧值。最后,参数的析构函数触发,清理lhs 的旧内存。请注意,这实际上不是 复制-and-swap 而是 move-and-swap。

也就是说,你现在所拥有的是不正确的。 std::swap 的默认实现将在内部尝试使用移动构造函数来移动元素,这会导致无限递归循环。您必须重载 std::swap 才能使其正常工作。

你可以在网上看到这个here at ideone。

有关详细信息,请参阅this question 及其对“四点半规则”的讨论。

希望这会有所帮助!

【讨论】:

我认为您应该挽救我的程序,应用您的修复程序,并将其作为示例发布在这里。这将是一个非常好的答案。 @Tomalak Geret'kal- 好主意。固定。 +1。 :) 我仍然不完全理解复制要求消失的地方。您实例化了一个std::move,但它不会仍然被复制到函数参数中吗?禁止优化? [编辑:嗯,好的,正在使用移动构造函数...] 不是 std::auto_ptr 的全部问题,当您应用 rhs 被修改的赋值运算符时,这不是您通常期望的对象。赋值不应该(在正常情况下)修改 rhs,因为它会混淆对象的用户。使用新的 std::unique_ptr 您必须显式移动对象,我希望用户在实现自己的移动操作时将其作为标准机制进行调整。 但是现在rhs 不见了(用你的话来说是“清空”)。我认为破坏分配对象的分配操作严重违反了最小意外原则......因此我的回答。【参考方案2】:

我认为这很好,但我真的不明白你为什么不这样做:

operator=(Foo&amp;&amp; rhs) // pass by rvalue reference not value

并为自己节省一步。

【讨论】:

除了一件事,move 语义不是 swap 语义,例如,参见cpp-next.com/archive/2009/09/your-next-assignment 所以基本上,您需要在移动方面实现swap(例如std::swap),而不是在交换方面的移动。 我主要关注将按值传递更改为按右值引用传递。我只是复制并粘贴了问题中的其他代码,并不打算解决这个问题。【参考方案3】:

接下来是意见,我并不真正符合 0x 标准,但我认为我有相当充分的理由支持我。

没有。 事实上,完全不支持分配是正确的。

考虑语义:

“assign”的意思是“使已经存在的B与A相同”。 “复制”的意思是“创建 B,并使其与 A 相同”。 “交换”的意思是“使 B 与 A 相同,同时使 A 与 B 相同”。 “移动”的意思是“使 B 与 A 相同,并摧毁 A。”

如果我们不能复制,那么我们就不能复制和交换。复制和交换是一种实现赋值的安全方式:我们创建与 A 相同的 C,将其与 B 交换(因此 C 现在是 B,B 与 A 相同),然后销毁 C (清理旧的 B 数据)。这根本不适用于移动和交换:我们不能在任何时候破坏 A,但移动会破坏它。此外,移动不会产生新值,所以我们将 A 移动到 B 中,然后就没有什么可以交换了。

除此之外 - 使类不可复制的原因肯定不是因为“创建 B”会出现问题,而是因为“导致它与 A 相同”会出现问题。 IOW,如果我们不能复制,我们为什么要指望能够分配?

【讨论】:

您希望能够分配一个右值。如 Foo f = returns_Foo_byval();这是移动任务。 Foo f; f = returns_Foo_byval(); 将是一个分配,但您拥有的行将是一个构造,除非它们在 0x 中的变化比他们需要的要大得多。 我不同意。在许多情况下(例如分配给函数的返回值),您希望支持从右值分配。您还可能遇到容器中不可复制、可移动的对象等情况,其中重估分配对于正确操作至关重要。最后,像 unique_ptr 这样的对象专门通过这样做来工作。 同意,这违反了(复制)赋值的意思,使用复制赋值语法来伪造移动赋值。因此,每次编译器尝试将其作为复制分配调用时,它都会失败。标准刚刚摆脱了auto_ptr,没必要偷偷回来。 @templatetypedef “重估赋值”是什么意思?

以上是关于在可移动和不可复制的类上使用移动和交换习语是不是有意义的主要内容,如果未能解决你的问题,请参考以下文章

在自我分配期间,复制和交换习语如何工作?

noexcept 交换和移动具有互斥锁的类

使类不可复制*和*不可移动

移动分配与标准复制和交换不兼容

C++复制、移动、交换、赋值和析构函数的继承?我需要哪个

如何在不同的类上使用相同的属性和方法? [复制]