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

标签 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. 如何以有意义的方式链接移动赋值以返回左值引用?

最佳答案

是的,您可以自由地从重载的赋值运算符返回任何您想要的内容,因此您的 MyMovable 没问题,但代码的用户可能会对意外的语义感到困惑。

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

b = std::move(a);

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

如果你以这种方式链接它:

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

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

现在

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

它按照您的预期工作,在两种情况下调用移动赋值,并且很明显 b 的状态也被移动。但你为什么要这么做呢?您可以使用 c = std::move(a); 直接将 a 的状态转移到 c,而无需清除 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)));
}

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

我不知道您是否确实想这样做。

关于c++ - 为什么重载的移动赋值运算符返回左值引用而不是右值引用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58332252/

相关文章:

c++ - 使用 C++ 进行 Linux 编程

c++ - 如何通过反汇编从 C++ 函数中获取 "lea"指令?

c++ - `intmax_t` 在具有 64 位 `long int` 和 `long long int` 的平台上应该是什么?

c++ - Cpp 98 标准中的 std::cout 格式

python - 将 C++ boost::variant 暴露给 Python 时出错

C++ 宏有条件地编译代码?

c++ - 将函数作为参数传递以避免重复代码

c++ - 我不明白这个链接错误

c++ - 表达式模板和求和符号

c++ - C++ 中的前向声明——什么时候重要?