为什么C++的lvalue对象不能绑定到rvalue引用 (&&)?

16

移动语义的概念是你可以从另一个临时对象(由rvalue引用引用)中获取一切,并将那个"一切"存储在你的对象中。这有助于避免深度复制,其中单个构造足够 -- 所以你在rvalue对象中构造事物,然后只需将其移动到生存时间更长的对象中。

为什么C++不允许将左值对象绑定到rvalue引用?两者都允许我更改所引用的对象,因此在访问所引用的对象的内部方面对我来说没有区别。

我唯一能猜测的原因是函数重载模糊问题。


2
你是否在问为什么不能这样写 void foo(vector & bar) {} foo(create temp vector here); - NathanOliver
2
你知道 std::move 吗? - Jarod42
@NathanOliver,OP正在询问相反的情况——void foo(vector&& bar) {} foo(already existing vector);;-) - L. F.
2个回答

30
“为什么C++不允许将左值对象绑定到右值引用?”假设您的意思是“为什么C++不允许将右值引用绑定到左值对象”:实际上是可以的。只是它不是自动的,所以您必须使用std::move来明确表示。为什么?因为否则一个无害的函数调用可能会意外地破坏您没有预料到的东西:”
Class object(much,state,many,members,wow);
looks_safe_to_me(object);
// oh no, it destructively copied my object!

对比

Class object(much,state,many,members,wow);
obviously_destructive(std::move(object));
// same result, but now the destruction is explicit and expected

关于破坏性复制的说明:为什么我在上文中使用“破坏性”和“破坏”这些词,我并不是指对象析构函数结束了其生命周期:只是它的内部状态已经移动到了一个新的实例。它仍然是一个有效的对象,但不再持有之前昂贵的状态。

术语说明:让我们看看上面关于lvalue,rvalue等术语的不准确使用是否可以澄清。

为了记录,引用cppreference

  • lvalue

    一个表达式,具有身份无法移动

    因此,不存在lvalue对象,但是存在由lvalue表达式本地命名(或引用)的对象。

  • rvalue

    一个表达式,可以是prvalue或xvalue。它们可以被移动。它们可能具有身份,也可能没有。

    • prvalue(纯rvalue)大致是指引用未命名临时对象的表达式:我们不能将lvalue表达式转换为其中之一(如果我理解正确)。

    • xvalue(到期值)是

      一个表达式,具有身份并且可以被移动

      这明确包括std::move的结果。

所以实际发生的是:

  • 一个对象存在
  • 该对象由lvalue表达式在本地标识,该表达式不能被移动(以保护我们免受意外副作用的影响)
  • std::move产生一个xvalue表达式(可移动),引用与lvalue表达式相同的对象
  • 这意味着诸如变量(由lvalue表达式命名)的对象不能被隐式移动,而必须通过显式xvalue表达式(例如std::move显式移动。
  • 匿名临时对象可能已经由prvalue表达式引用,并且可以隐式移动

2
当您使用std::move时,您并不是将rvalue引用绑定到lvalue上。您正在将lvalue强制转换为rvalue,然后将rvalue引用绑定到它上。 - Jerry Coffin
中间不是xvalue表达式吗?无论如何,最终结果是一个rvalue引用别名,它与我们之前确定为lvalue的对象重叠... - Useless
2
@无用的,但是对象没有值类别,表达式有。表达式object是一个左值,表达式std::move(object)是一个右值。 - TartanLlama
好的观点:我需要努力提高词汇量,以便清晰地讨论这个问题。 - Useless
1
我希望我能给这个点赞两次 :-) - 这是我见过的最简单和最短的解释,指出了lvalue、prvalues、xvalues之间的区别(如果加上glvalues就更完整了)。当然,我指的是你的评论,而不是引用。 - andreee

4
基本上,需要一个机制来区分可以移动的值和不能移动的值(即需要复制)。允许将rvalue和lvalue都绑定到lvalue引用会使这种区分变得不可能。因此,绑定到rvalue引用的值可以被移动(不一定总是要移动,但是允许),而lvalue可以绑定到lvalue引用中并且不能被移动。std::move存在的目的是允许在值类别之间进行转换(到rvalue)以允许移动发生。注意:const lvalue引用(const T&)可以绑定到rvalue(临时对象)和lvalue,因为所引用的对象不能更改(它标记为const,因此无论如何都不能移动)。

有些历史(追溯到C++的早期)解释了为什么临时对象一开始不能绑定到非const左值引用...细节有点模糊,但是有一些理由是因为修改临时对象没有意义,因为它会在当前语句结束时被销毁。此外,你可能会误以为自己正在修改一个左值,而实际上不是这样——代码的语义可能/会出错并导致错误。还有其他原因与地址、文字等有关。这是在移动及其语义确立之前,也是移动和其语义的一些动机。


我的总体印象是,在试图修复某些问题时,C++委员会只是制造了更大的混乱。在引入T&&之前,可以使用引用来修改传递的对象(无论发生何种修改都不重要)。这意味着在每种情况下,当传递一个非const引用时,都应该检查传递的实体发生了什么。他们实际上试图建立T&不应该被修改(即其内容不应被获取)的概念。这个概念是错误的。应该使用const T&来实现这一点。 - Kirill Kobelev
他们复杂规则的净效应将是相反的。许多程序员会编写很多有缺陷的代码,因为他们不会试图理解语言设计的细节,而是以某种方式编写“某些东西”。是的,有时他们的代码会起作用。在其他情况下 - 一如既往。 - Kirill Kobelev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接