为什么重载的移动赋值运算符返回左值引用而不是右值引用?

时间:2019-10-10 23:14:14

标签: c++ c++11

所以这实际上只是我在语义上无法理解的东西。分配链接对于复制语义很有意义:

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

abc均为100

尝试使用移动语义进行类似操作吗?只是不起作用或没有道理:

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

这甚至不编译,因为a = b是一个副本分配,已被删除。我可以提出一个论点,即执行最终表达式之后,*a == 100b == 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';
}

有趣的是,此示例actually works如我所愿,并提供以下输出:

  

a = 100 b = 0 c = 0

我不确定是否 有效,或者为什么有效,尤其是因为正式的移动分配不是用这种方式定义的。坦率地说,这在本已混乱的类类型语义行为世界中增加了更多的混乱。

因此,我在这里提出了一些建议,因此我将其简化为一系列问题:

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

4 个答案:

答案 0 :(得分:6)

是的,您可以从重载的赋值运算符中随意返回任何内容,因此MyMovable很好,但是您的代码用户可能会被意料之外的语义所迷惑。

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

b = std::move(a);

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

如果以这种方式链接它:

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

然后,您会期望b不会失去其状态,因为您从未对其应用std::move。但是,如果您的移动赋值运算符通过rvalue-reference返回,则实际上会将a的状态移到b中,然后左侧赋值运算符也会调用move赋值,从而转移状态b(之前是a)到c。这令人惊讶,因为现在ab都具有空/未指定状态。相反,我希望将a移到b并复制到c中,如果移动分配返回左值引用,这正是发生的情况。

现在为

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

它按预期工作,在两种情况下都调用移动分配,很显然b的状态也被移动了。但是为什么要这么做呢?您可以直接通过ac的状态转移到c = 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)));
}

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

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

答案 1 :(得分:5)

  
      
  1. 两种形式的赋值运算符都有效吗?
  2.   

它们的格式正确且行为明确。但是,从没有右值限定的函数返回对*this的右值引用将是非常规的,并且可能不是一个好的设计。

如果:

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

导致b被移走。令人惊讶的API并不是一个好功能。

  
      
  1. 您何时会使用其中一个?
  2.   

通常,您永远都不想返回对*this的右值引用,除非是从右值限定函数中返回。从右值限定函数中,最好根据上下文返回右值引用甚至是prvalue。

  
      
  1. 移动分配链接的东西吗?如果是这样,什么时候/为什么要使用它?
  2.   

我从未见过它的使用,也无法想到一个有吸引力的用例。

  
      
  1. 如何甚至返回左值引用的链移动分配
  2.   

与您展示的一样,有std::move个。

  

以一种有意义的方式?

我不确定是否有办法向其中引入含义。

答案 2 :(得分:3)

  
      
  1. 两种形式的赋值运算符都有效吗?
  2.   

赋值运算符通常是常规方法,不需要将左值引用返回给自身类型,返回voidchar也是有效的。

为避免意外,我们尝试模仿内置方法,因此为了允许链接分配,我们将左值引用返回为自身类型。

  
      
  1. 您何时会使用其中一个?
  2.   

我个人只会使用MyMovable& operator=(MyMovable const&)

  
      
  1. 移动分配链接的东西吗?如果是这样,什么时候/为什么要使用它?
  2.   

我不这样。

但是要允许语法a = std::move(b) = std::move(c);,您可以这样做:

    MyMovable& operator=(MyMovable const&) = delete;
    MyMovable&& operator=(MyMovable&& other) &&;
    MyMovable& operator=(MyMovable&& other) &;

答案 3 :(得分:1)

  

两种形式的赋值运算符都有效吗?

我相信,按照标准,但要非常小心。当您从operator=返回右值引用时,是说它可以自由修改。这很容易导致令人惊讶的行为。例如,

void foo(const MyMovable& m) { }
void foo(MyMovable&& m) {
    m.value_ = 666; // I'm allowed to do whatever I want to m
}

int main() {
    MyMovable d;
    foo(d = MyMovable{ 200 }); // will call foo(MyMovable&&) even though d is an lvalue!
    std::cout << "d=" << *d << '\n'; // outputs 666
}

解决此问题的正确方法可能是这样定义您的operator=

MyMovable&& operator=(MyMovable&& other) && {...

现在这仅在* this已经是一个右值时才有效,并且由于在左值上使用operator=,因此上面的示例将无法编译。但是,这样就不允许您的链接移动运算符起作用。我不确定如何既允许移动操作员链接,又可以防止出现上述示例中的行为。

  

您何时会使用其中一个?

我认为我永远不会返回* this的右值引用。如上例所示,这很容易使调用者感到惊讶,而且我不确定我是否真的想启用移动分配链接。

  

移动分配链接了东西吗?如果是这样,什么时候/为什么要使用它?

我从不使用分配链。它节省了一些字符,但我认为它使代码不那么明显,并且正如该问题所表明的,在实践中可能会有些棘手。

  

您如何甚至以有意义的方式返回左值引用的链移动分配?

如果必须这样做,我将使用您上面使用的形式:a = std::move(b = std::move(c));这很明显。