斯科特·迈耶斯谈价值

Scott Meyers on Rvalueness

本文关键字:斯科特      更新时间:2023-10-16

我观看了斯科特·迈耶斯(Scott Meyers)关于通用参考文献的非常翔实的视频,其中我学到了我对Rvalue参考,移动和转发的大部分了解。有一次,他谈论的是右值而不是变量的类型,他说了一些大意是"右与类型无关"的话。

我知道你可以有这样的方法:

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),括号之间结果表达式的类型似乎与右值无关

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

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表示的对象类型(以及左值*os的动态类型)仅在运行时已知

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

在这里,由运算符 new 创建的数组的类型也只在运行时知道,并且(谢天谢地)可以并且不能转义到静态C++类型系统。臭名昭著的别名规则(粗略地说,禁止从不兼容的左值读取对象)谈到对象的"动态类型",大概是因为它想强调运行时关注的问题。但严格来说,说一个对象的"动态类型"是很奇怪的,因为一个对象没有"静态类型"。

  • 变量或成员的声明类型

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

struct A {
A() { }
int a;
};
const A *a = new const A;
volatile const A *va = a;

此处,表达式a->a的类型为const int,但a->a解析为的成员的声明类型具有类型int(成员实体)。用a->a表示的对象类型具有类型const int,因为我们使用new表达式创建了一个const A对象,因此所有非静态数据成员都隐式const子对象。在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]


那么右值可以是什么类型呢?右值是表达式。

int &&x = 0;
int y = std::move(x);
int z = x;

在这里,我们有右值0std::move(x)。两个右值的类型均为intz的初始值设定项中显示的表达式x是一个左值,即使它引用的与右值std::move(x)引用的同一对象也是如此。


您在问题中关于分别用右值或左值调用的重载函数的最后一点很有趣。仅仅因为右值引用写为int &&并不意味着右值具有类型int。它们被称为右值引用,因为您可以使用右值初始化它们,并且语言更喜欢初始化而不是使用右值初始化左值引用。


此外,查看名称形式的表达式(右值)可能很有用

enum A { X };
template<int Y> struct B { };

如果使用XY,它们是右值。但这些案例是我能想到的唯一案例。

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

最后一点值得牢记:价值或右价值 的表达式与其类型无关。

他在这里解释道。 他的主要观点是:

表达式的类型不会告诉您它是左值还是 一个右值。

在他的结束语中:

在类型声明中,"&&"表示右值引用或 通用引用 – 可以解析为左值的引用 引用或右值引用。通用引用始终具有 形式 T&&&对于某些推断的 T 型。

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

此处使用类型T来表示任何类型。 它可以是int&&,或double&&,或Widget&&- 没关系。

首先,让我们将讨论限制在纯右值引用上,而将通用引用放在一边。我们不是在谈论template <typename T> ... T &&var ...

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

凡是受此提法约束的,当它与该提法订有约束力时,都是"一次性的"。它超出了范围。如果你在绑定它的那一刻修改了它,没有人会知道。在它被绑定时,没有其他提及它。

这使我们能够使用右值引用来获得一些自由,而这些自由是我们无法使用其他类型的变量的。想到的第一个用途是使用 swap() 窃取其内容。