首页 > 解决方案 > 为什么重载的移动赋值运算符返回左值引用而不是右值引用?

问题描述

所以这真的只是我无法从语义上理解的东西。赋值链对复制语义有意义:

int a, b, c{100};
a = b = c;

a, b,c都是100.

用移动语义尝试类似的事情?只是不起作用或没有意义:

std::unique_ptr<int> a, b, c;
c = std::make_unique<int>(100);
a = b = std::move(c);

这甚至不能编译,因为a = b是一个复制分配,它被删除了。我可以提出一个论点,即在最终表达式执行后,*a == 100andb == nullptrc == nullptr. 但这不是标准所保证的。这并没有太大变化:

a = std::move(b) = std::move(c);

这仍然涉及复制分配。

a = std::move(b = std::move(c));

这确实有效,但从语法上讲,它与复制分配链有很大的不同。

声明重载的移动赋值运算符涉及返回左值引用:

class MyMovable
{
public:
    MyMovable& operator=(MyMovable&&) { return *this; }
};

但为什么它不是右值引用?

class MyMovable
{
public:
    MyMovable() = default;
    MyMovable(int value) { value_ = value; }
    MyMovable(MyMovable const&) = delete;
    MyMovable& operator=(MyMovable const&) = delete;
    MyMovable&& operator=(MyMovable&& other) {
        value_ = other.value_;
        other.value_ = 0;
        return std::move(*this);
    }
    int operator*() const { return value_; }
    int value_{};
};

int main()
{
    MyMovable a, b, c{100};
    a = b = std::move(c);

    std::cout << "a=" << *a << " b=" << *b << " c=" << *c << '\n';
}

有趣的是,这个例子实际上像我预期的那样工作,并给了我这个输出:

a=100 b=0 c=0

我不确定它是否不应该工作,或者为什么会这样,特别是因为正式的移动分配不是这样定义的。坦率地说,这只会在已经令人困惑的类类型语义行为世界中增加更多的混乱。

所以我在这里提出了一些事情,所以我将尝试将其浓缩为一组问题:

  1. 两种形式的赋值运算符都有效吗?
  2. 您什么时候使用其中一种?
  3. 移动分配链接是一回事吗?如果是这样,您何时/为什么会使用它?
  4. 您甚至如何链接以有意义的方式返回左值引用的移动赋值?

标签: c++c++11

解决方案


是的,您可以自由地从重载的赋值运算符中返回您想要的任何内容,所以您MyMovable很好,但是您的代码的用户可能会被意外的语义混淆。

我看不出有任何理由在移动分配中返回右值引用。移动分配通常如下所示:

b = std::move(a);

之后,b应该包含 的状态a,而a将处于某个空或未指定的状态。

如果以这种方式链接它:

c = b = std::move(a);

那么你会期望b不会失去它的状态,因为你从未申请std::move过它。但是,如果您的移动赋值运算符通过右值引用返回,那么这实际上会将的状态移动ab,然后左侧的赋值运算符也会调用移动赋值,将ba之前的)的状态转移到c。这是令人惊讶的,因为现在两者都a具有b空/未指定状态。相反,我希望a被移动b并复制到c中,这正是移动赋值返回左值引用时会发生的情况。

现在为

c = std::move(b = std::move(a));

它按您的预期工作,在两种情况下都调用移动分配,很明显状态也b被移动了。但是你为什么要这样做呢?您可以直接将a' 的状态转移到with ,而无需在此过程中清除' 的状态(或者更糟糕的是,将其置于不可直接使用的状态)。即使你想要那样,它会更清楚地表述为一个序列cc = std::move(a);b

c = std::move(a);
b.clear(); // or something similar

至于

c = std::move(b) = std::move(a)

起码很明显是b被移了,但是好像现在的状态是b被移了,而不是右移之后的那个,双移是多余的。正如您所注意到的,这仍然调用左侧的复制分配,但如果您真的想在这两种情况下都调用移动分配,您可以通过右值引用返回并避免上述问题c = b = std::move(a),您会需要在中间表达式的值类别上进行区分。这可以通过以下方式完成:

MyMovable& operator=(MyMovable&& other) & {
    ...
    return *this;
}
MyMovable&& operator=(MyMovable&& other) && {
    return std::move(operator=(std::move(other)));
}

&and限定符表示如果调用成员函数的表达式是左值或右值,&&则应使用特定的重载。

我不知道您是否真的想这样做。


推荐阅读