Scott Meyers关于RValue的性质

8
我观看了Scott Meyers非常信息丰富的有关通用引用的视频,在其中我学到了大部分有关Rvalue引用、移动和转发的知识。在其中他谈到了rvalueness与变量类型相对立的概念,并且他说过“rvalueness与类型无关”。
我理解你可以拥有这样一个方法:
void func(MyType&& rRef)
{
    // Do domething with rRef...
}

这里的rRef是一个左值,因为它可以被识别,其地址可以被获取等,即使它的类型是MyType&&

但是一个右值不能是任何类型,对吧?我的意思是,它只能是MyType&&,对吗?从这个意义上说,我认为类型并不完全独立于右值性。也许我漏掉了什么。

更新:我的观点可以更清晰地表达如下。如果在func()中,我调用了两个重载函数之一,定义如下:

void gunc(MyType&& rRef)
{
   // ...
}

void gunc(MyType& lRef)
{
   // ...
}

即通过调用gunc(std::move(rRef))gunc(rRef),似乎括号内结果表达式的类型与rvalue无关。

3
在表达式中,引用从类型“进一步分析之前”[expr] / 5中删除。它们仅用于区分某些表达式的值类别(例如返回值)。因此,作为一个id-expression的rRef是类型MyType。R/Lvalueness是表达式的属性,而不是类型的属性。 - dyp
1
@KristianD'Amato 这个答案也可能为您澄清问题。它触及了您更新后的问题所问的内容。 - Zac Howland
顺便提一下,DyP,我所指的是在 func() 内部调用行的括号,而不是 gunc() 的参数声明。 - Kristian D'Amato
2
@KristianD'Amato 啊!好的。但是无论如何,std::move(rRef)rRef(作为表达式)的类型都只是int。左/右值用于选择重载(在这种情况下:左值无法绑定到MyType&&,右值无法绑定到MyType&,因此每种情况只有一个可行的重载)。 - dyp
4
术语:表达式的“左值性”或“右值性”是其“值类别”。正如您所说,它与“类型”是分开的。 - Mike Seymour
显示剩余10条评论
3个回答

4

表达式的类型没有任何引用痕迹。因此,如果我们暂时假设引用可以具有引用类型,那么我们将得到以下结果:

int a = 0;
int &ra = a;

int c = a + 42;
int d = ra + 42;

在上述代码中,表达式a 将会 有类型int,而表达式ra 将会 有类型int&。在规范的几乎所有关于表达式类型的规则中,例如规则说“表达式 E 必须是类型 X” ,我们都需要添加"...或者引用类型 X"(考虑强制类型转换运算符)。因此,我猜这会是太过繁琐,不便于使用。
C++有以下类型
  • 表达式的静态类型

如果没有明确说明指的是动态类型,则称为“表达式的类型”。这是一个表达式的属性,在编译时,抽象掉了表达式所指的内容。例如,如果a引用了一个int&int变量,或者是字面值0,那么所有这些表达式都有类型int

  • 左值表达式的动态类型

这是左值表达式所引用的非基类对象的类型。

ofstream fs("/log");
ostream &os = fs;

在这里,os静态类型ostream,而动态类型ofstream

  • 对象或引用的类型

这是一个对象或引用实际具有的类型。一个对象总是有单一类型,并且它的类型永远不会改变。但是,什么对象存在于哪个位置只有在运行时才知道,因此通常,“对象的类型”也是一个运行时的事情。

ostream *os;
if(file)
  os = new ofstream("/log");
else
  os = new ostringstream;

*os表示的对象类型(以及lvalue *os的动态类型)仅在运行时知道。

int *p = new int[rand() % 5 + 1];

在这里,通过operator new创建的数组类型只在运行时才知道,并且(幸运的是)不会逃逸到静态C++类型系统中。臭名昭着的别名规则(大致上禁止从不兼容的lvalue读取对象)涉及到对象的"动态类型",可能是因为它想强调运行时关注点的重要性。但严格来说,说一个对象的"动态类型"很奇怪,因为一个对象没有"静态类型"。
  • 变量或成员的声明类型

这是你在声明中给出的类型。与对象类型或表达式类型相比,这有时可能略有不同。

struct A {
  A() { }
  int a;
};

const A *a = new const A;
volatile const A *va = a;

这里,表达式 a->a 的类型为const int,但是 a->a 解析到的成员实体声明的类型为 int。因为我们用 new 表达式创建了一个const A对象,所以所有非静态数据成员都是隐式地const子对象,对象的类型为const int。 在 va->a 中,该表达式的类型为volatile const int a,变量的声明类型仍然为 int,引用的对象类型仍然为 const int
当你说“a的类型”,并且将“a”声明为 “int &a”时,你必须总是说明你所说的“a”的类型是什么。你是指表达式吗?那么“a”的类型为int。甚至会更糟。想象一下“int a[10]”;这里,“a”的表达式类型为int*int[10],这取决于你是否认为在你的表达式中进行了数组到指针转换,当你询问“a”的类型时。如果你询问“a”所引用的变量的类型,那么答案唯一地是intint[10]
那么rvalue可能具有什么类型呢?Rvalues是表达式。
int &&x = 0;
int y = std::move(x);
int z = x;

这里有两个 rvalue,一个是 0,另一个是 std::move(x),它们都是类型为 int 的 rvalue。对于变量 z 的初始化,出现在赋值号左侧的 x 是一个 lvalue,尽管它引用了与 rvalue std::move(x) 相同的对象。
你问题中关于重载函数如何处理 rvalue 和 lvalue 的最后一点很有趣。虽然 rvalue 引用写作 int &&,但这并不意味着 rvalues 的类型是 int。它们被称为 rvalue 引用是因为你可以使用 rvalues 来初始化它们,并且语言更喜欢使用 rvalue 初始化 rvalue 引用而不是 lvalue 引用。
此外,看到表达式中以名称形式出现的 rvalues 可能很有用。
enum A { X };
template<int Y> struct B { };

如果您使用 X 或者 Y,它们是右值。但这些似乎是我能想到的唯一情况。

谢谢Johannes。这是非常详细的回答,解释了我的困惑。我会花些时间,并将其选为正确答案! - Kristian D'Amato
两个问题,如果您不介意的话:1. 如果可能的话,表达式的动态类型和对象/引用的类型(大概是由第一个表达式引用的)何时分离?2. 您写道“仅仅因为右值引用被写成 int &&,并不意味着右值具有 int 类型”。您能详细解释一下吗? - Kristian D'Amato
动态类型表达式和对象/引用的类型(第一个表达式所指的对象/引用)是否分离,如果有,什么时候会分离?回答:不确定您是不是指这个,但如果有类和基类,则可能会发生这种情况。 因此,例如 ofstream of("/log"); ostream &os = of; 这里,“os” 是指在“of”所指的完整 ofstream 对象中的类型为 ostream 的基类子对象。 - Johannes Schaub - litb
我不知道“refers”和“denotes”的确切含义。在定义lvalue的“dynamic type”的含义时,规范使用了“refers”:“由glvalue表达式所表示的glvalue所引用的最派生对象(1.8)的类型”。严格来说,我认为上面的“os”并没有真正“引用”最派生对象,而只是其中一个基类子对象。虽然我希望你知道我想说什么 :) 在某种意义上,它“接触”了最派生对象 :) - Johannes Schaub - litb

2

我认为你省略了他的一部分引文:

还有一个值得记住的要点:表达式的lvalue或rvalue与其类型无关。

他在这里进行了解释。他的主要观点是:

表达式的类型不能告诉你它是lvalue还是rvalue。

在他的结论中:

在类型声明中,“&&”表示rvalue引用或通用引用 - 可能解析为lvalue引用或rvalue引用的引用。通用引用始终具有形式T&&,其中T是某个推断类型。

引用折叠是导致通用引用(在引用折叠发生的情况下实际上只是rvalue引用)有时解析为lvalue引用,有时解析为rvalue引用的机制。它发生在指定的上下文中,在编译期间可能会出现对引用的引用的情况。这些上下文是模板类型推断、auto类型推断、typedef形成和使用以及decltype表达式。

这里使用的类型T是指任何类型。它可以是int&&double&&Widget&& - 没有关系。


或者说,“任何具有名称的值在表达式中始终是一个左值(在其初始构造之后)”。它可以是一个右值引用,但这仅仅决定了它可以绑定到什么上。 - Yakk - Adam Nevraumont
@Yakk,我不确定我理解你评论的第二部分。你能再详细说明一下吗? - Zac Howland
foo get_foo(); foo&& tmp = get_foo(); 是合法的,而 foo& tmp = get_foo(); 不是,因为类型为 foo& 的变量不能绑定到临时对象,而类型为 foo&& 的变量可以。请参见 http://ideone.com/zrP9oY 获取示例。这些,以及 decltype,几乎是 foo&&foo& 变量行为之间的唯一区别。 - Yakk - Adam Nevraumont

0

首先,让我们将讨论限制在普通右值引用上,不考虑通用引用。我们不讨论 template <typename T> ... T &&var ...

就常规的 Type &&var = somevalue; 右值引用情况而言,我认为它的含义如下:

无论绑定到这个引用的是什么,在绑定到引用时,它都是“可丢弃”的。它即将超出作用域。如果你在绑定的时候对它进行了修改,没有人会知道。在它被绑定时,没有其他引用指向它。

这使我们能够对右值引用采取一些其他类型变量不能采取的自由。首先想到的用途是使用 swap() 从中窃取其内容。


1
这使我们可以在rvalue引用方面采取一些自由,而其他类型的变量则不行。首先想到的用途是使用swap()来窃取其内容。虽然var被声明为rvalue引用类型的变量,但它是一个lvalue(作为id-expression)。从我在评论中发布的示例中可以看出:string&& rv = string(); string x = rv; string y = rv; 不能安全地从rvalue引用变量中移动,因为移动后仍可能被引用。 - dyp
非常正确。这就是为什么我一直说“当它被绑定时”。你必须小心,不要在绑定和超出范围之间创建更多的rvalue引用。另一方面,我认为你在我的答案评论中发布的示例相当安全。如果xy是引用,则会不安全。 - user3458
抱歉 - 我忘记了复制构造函数不一定要进行实际复制。因此,是的,你的例子在一般意义上是不安全的,尽管对于字符串来说应该是安全的。 - user3458
1
我的意思是:如果 rv 一个右值,那么 string x = rv; 将调用 移动构造函数。然后,在一般情况下,x == y 将不成立。但幸运的是,rv 是一个左值,因此 string x = rv; 调用了拷贝构造函数(假设不会改变 rv)。 - dyp
string x = rv 中,rv 不能是右值:它是一个命名变量,因此是左值。 - user3458
我一直在根据反事实假设发表假设性陈述,即rv将成为一个右值。当然,在当前标准中它不是一个右值。我想指出的是,您不应该将右值引用类型的变量与非引用类型的变量区别对待。 - dyp

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