所以这确实是我无法从语义上理解的东西。赋值链对于复制语义有意义:
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 == 100
和 b == nullptr
和 c == 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
我不确定它是否不应该起作用,或者为什么它起作用,特别是因为正式的移动分配不是这样定义的。坦率地说,这只会在已经令人困惑的类类型语义行为世界中增加更多困惑。
所以我在这里提出了一些事情,所以我会尝试将其压缩为一组问题:
- 这两种形式的赋值运算符都有效吗?
- 您什么时候会使用其中之一?
- 移动分配链是一回事吗?如果是这样,您何时/为什么会使用它?
- 如何以有意义的方式链接移动赋值以返回左值引用?
最佳答案
是的,您可以自由地从重载的赋值运算符返回任何您想要的内容,因此您的 MyMovable
没问题,但代码的用户可能会对意外的语义感到困惑。
我认为没有任何理由在移动赋值中返回右值引用。移动分配通常如下所示:
b = std::move(a);
之后,b
应该包含 a
的状态,而 a
将处于某种空或未指定的状态。
如果你以这种方式链接它:
c = b = std::move(a);
那么您会期望 b
不会丢失其状态,因为您从未对其应用 std::move
。但是,如果您的移动赋值运算符通过右值引用返回,那么这实际上会将 a
的状态移动到 b
中,然后左侧赋值运算符也会调用 move赋值,将 b
的状态(之前是 a
)转移到 c
。这是令人惊讶的,因为现在 a
和 b
都具有空/未指定状态。相反,我希望将 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/