所以这实际上只是我在语义上无法理解的东西。分配链接对于复制语义很有意义:
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
我不确定是否 有效,或者为什么有效,尤其是因为正式的移动分配不是用这种方式定义的。坦率地说,这在本已混乱的类类型语义行为世界中增加了更多的混乱。
因此,我在这里提出了一些建议,因此我将其简化为一系列问题:
答案 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
。这令人惊讶,因为现在a
和b
都具有空/未指定状态。相反,我希望将a
移到b
并复制到c
中,如果移动分配返回左值引用,这正是发生的情况。
现在为
c = std::move(b = std::move(a));
它按预期工作,在两种情况下都调用移动分配,很显然b
的状态也被移动了。但是为什么要这么做呢?您可以直接通过a
将c
的状态转移到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)
- 两种形式的赋值运算符都有效吗?
它们的格式正确且行为明确。但是,从没有右值限定的函数返回对*this
的右值引用将是非常规的,并且可能不是一个好的设计。
如果:
a = b = std::move(c));
导致b
被移走。令人惊讶的API并不是一个好功能。
- 您何时会使用其中一个?
通常,您永远都不想返回对*this
的右值引用,除非是从右值限定函数中返回。从右值限定函数中,最好根据上下文返回右值引用甚至是prvalue。
- 移动分配链接的东西吗?如果是这样,什么时候/为什么要使用它?
我从未见过它的使用,也无法想到一个有吸引力的用例。
- 如何甚至返回左值引用的链移动分配
与您展示的一样,有std::move
个。
以一种有意义的方式?
我不确定是否有办法向其中引入含义。
答案 2 :(得分:3)
- 两种形式的赋值运算符都有效吗?
赋值运算符通常是常规方法,不需要将左值引用返回给自身类型,返回void
,char
也是有效的。
为避免意外,我们尝试模仿内置方法,因此为了允许链接分配,我们将左值引用返回为自身类型。
- 您何时会使用其中一个?
我个人只会使用MyMovable& operator=(MyMovable const&)
- 移动分配链接的东西吗?如果是这样,什么时候/为什么要使用它?
我不这样。
但是要允许语法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));
这很明显。