ChatGPT解决这个技术问题 Extra ChatGPT

什么是 rvalues、lvalues、xvalues、glvalues 和 prvalues?

在 C++03 中,表达式是右值或左值。

在 C++11 中,表达式可以是:

右值 左值 x 值 右值 右值

两个类别变成了五个类别。

这些新的表达方式是什么?

这些新类别与现有的右值和左值类别有何关系?

C++0x 中的右值和左值类别是否与 C++03 中的相同?

为什么需要这些新类别? WG21 的众神只是想迷惑我们这些凡人吗?

@Philip Potter:在 C++03 中?是的。左值可以用作右值,因为存在标准的左值到右值的转换。
@Tyler:“如果你可以分配给它,它是一个左值,否则,它是一个右值。” ->错了,您可以分配给类右值:string("hello") = string("world")
请注意,这是值类别。表达式可以有更多的属性。这些包括位域(真/假)、临时(真/假)和类型(它的类型)。
我认为 Fred 上面的链接比这里的任何答案都好。但是,链接已失效。已移至:stroustrup.com/terminology.pdf
在 C++ 中,即使你的类型也有类型

K
Kornel Kisielewicz

我想这份文件可能会作为一个不那么简短的介绍:n3055

整个屠杀从移动语义开始。一旦我们有了可以移动而不是复制的表达式,突然容易掌握的规则要求区分可以移动的表达式以及移动的方向。

从我基于草案的猜测来看,r/l 值的区别保持不变,只是在移动事物变得混乱的情况下。

他们需要吗?如果我们希望放弃新功能,可能不会。但是为了实现更好的优化,我们可能应该接受它们。

引用 n3055

左值(所谓的,历史上,因为左值可能出现在赋值表达式的左侧)指定一个函数或一个对象。 [示例:如果 E 是指针类型的表达式,则 *E 是一个左值表达式,指的是 E 指向的对象或函数。再举一个例子,调用返回类型为左值引用的函数的结果是左值。]

一个 xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。 xvalue 是某些涉及右值引用的表达式的结果。 [示例:调用返回类型为右值引用的函数的结果是 xvalue。]

左值(“广义”左值)是左值或 xvalue。

右值(所谓的,历史上,因为右值可能出现在赋值表达式的右侧)是一个 xvalue,一个临时对象或其子对象,或者一个不与对象关联的值。

prvalue(“纯”右值)是不是 xvalue 的右值。 [示例:调用返回类型不是引用的函数的结果是prvalue]

有问题的文件是这个问题的一个很好的参考,因为它显示了由于引入新命名法而发生的标准的确切变化。


谢谢,这个答案真的很有帮助!但是我的编译器不同意您的 xvalues 和 prvalues 示例;他们恰恰相反。通过右值引用返回给我一个prvalue,按值返回给我一个xvalue。你把它们弄混了,还是我的测试台坏了?我用 GCC 4.6.1、clang(来自 svn)和 MSVC 进行了尝试,它们都表现出相同的行为。
我使用此处的宏来测试各种表达式:stackoverflow.com/a/6114546/96963 可能是它们误诊了。
添加 xvalue 不是为了移动语义。只有同时使用左值和右值,移动语义、完美前向和右值引用仍然可以正常工作。我认为 xvalue 仅适用于 decltype 运算符:如果操作数表达式是 xvalue,则 decltype 给出右值引用的类型。
@MuhamedCicak“每个表达式都是左值或右值”:是的;并且标准(或文档 n3055)并没有说它是错误的。这句话被划掉的原因是您正在查看文档的两个版本之间的更改。这句话被删除了,因为在添加了更精确的解释后它变得多余了。
我的朋友们,我们正式去香蕉了。
1
11 revs, 11 users 77%

这些新的表达方式是什么?

FCD (n3092) 有一个很好的描述:

— 左值(历史上称为左值,因为左值可能出现在赋值表达式的左侧)指定函数或对象。 [ 示例:如果 E 是指针类型的表达式,则 *E 是一个左值表达式,指的是 E 指向的对象或函数。作为另一个示例,调用返回类型为左值引用的函数的结果是左值。 —end example ] — 一个 xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。 xvalue 是某些涉及右值引用的表达式的结果 (8.3.2)。 [ 示例:调用返回类型为右值引用的函数的结果是 xvalue。 —结束示例] — 左值(“广义”左值)是左值或 xvalue。 — 右值(历史上称为右值,因为右值可能出现在赋值表达式的右侧)是一个 xvalue、一个临时对象 (12.2) 或其子对象,或者一个与对象无关的值。 — prvalue(“纯”右值)是不是 xvalue 的右值。 [ 示例:调用返回类型不是引用的函数的结果是纯右值。诸如 12、7.3e5 或 true 之类的文字的值也是纯右值。 —end example ] 每个表达式都恰好属于此分类法中的基本分类之一:lvalue、xvalue 或 prvalue。表达式的这个属性称为它的值类别。 [ 注意:第 5 章中对每个内置运算符的讨论表明了它产生的值的类别以及它所期望的操作数的值类别。例如,内置赋值运算符期望左操作数是左值,右操作数是纯右值并产生左值作为结果。用户定义的运算符是函数,它们期望和产生的值的类别由它们的参数和返回类型决定。 ——尾注

不过,我建议您阅读整个第 3.10 节左值和右值。

这些新类别与现有的右值和左值类别有何关系?

再次:

https://i.stack.imgur.com/GNhBF.png

C++0x 中的右值和左值类别是否与 C++03 中的相同?

右值的语义随着移动语义的引入而发展。

为什么需要这些新类别?

这样就可以定义和支持移动构造/分配。


我喜欢这里的图表。我认为以“每个表达式恰好属于此分类法中的基本分类之一:左值、xvalue 或 prvalue”作为答案可能会很有用。然后很容易使用该图来显示这三个基本类组合成glvalue和rvalue。
“is glvalue”等价于“is not prvalue”,“is rvalue”等价于“is not lvalue”。
这个对我帮助最大:bajamircea.github.io/assets/2016-04-07-move-forward/…(价值类别的维恩图)
@AaronMcDaid 嗨,如果您/有人可以回答,请快速提问...为什么不将 glvalue 命名为 lvalue 并将 lvalue 命名为 plvalue,以保持一致?
@JohnP 谢谢你,维恩图终于让我明白了这些分类图在说什么。令人抓狂的是,所有这些文件都提供了分类图,甚至没有说明箭头的含义。这让我完全蒙在鼓里。
P
Potatoswatter

我将从你的最后一个问题开始:

为什么需要这些新类别?

C++ 标准包含许多处理表达式的值类别的规则。一些规则区分左值和右值。例如,当涉及到重载决议时。其他规则对 glvalue 和 prvalue 进行了区分。例如,您可以有一个不完整或抽象类型的泛左值,但没有不完整或抽象类型的纯右值。在我们使用这个术语之前,实际上需要区分 glvalue/prvalue 的规则指的是 lvalue/rvalue,它们要么是无意错误的,要么包含许多对规则 a la “的解释和例外”......除非 rvalue 是由于未命名的右值引用..."。所以,给glvalues和prvalues的概念自己命名似乎是个好主意。

这些新的表达方式是什么?这些新类别与现有的右值和左值类别有何关系?

我们仍然有与 C++98 兼容的术语左值和右值。我们只是将右值分为两个子组,xvalues 和 prvalues,我们将 lvalues 和 xvalues 称为 glvalues。 Xvalues 是一种用于未命名右值引用的新型值类别。每个表达式都是以下三个之一:左值、xvalue、prvalue。维恩图如下所示:

    ______ ______
   /      X      \
  /      / \      \
 |   l  | x |  pr  |
  \      \ /      /
   \______X______/
       gl    r

函数示例:

int   prvalue();
int&  lvalue();
int&& xvalue();

但也不要忘记命名的右值引用是左值:

void foo(int&& t) {
  // t is initialized with an rvalue expression
  // but is actually an lvalue expression itself
}

N
Nicol Bolas

为什么需要这些新类别? WG21 的众神只是想迷惑我们这些凡人吗?

我不认为其他答案(虽然很多都很好)真正抓住了这个特定问题的答案。是的,存在这些类别等以允许移动语义,但存在复杂性是有原因的。这是在 C++11 中移动内容的一个不可违反的规则:

只有在毫无疑问是安全的情况下才能移动。

这就是存在这些类别的原因:能够在安全的地方谈论价值观,并在不安全的地方谈论价值观。

在最早版本的 r 值引用中,移动很容易发生。太容易了。很容易,当用户并不真正想要时,有很多潜在的隐式移动东西。

以下是可以安全移动某物的情况:

当它是临时对象或子对象时。 (prvalue) 当用户明确表示要移动它时。

如果你这样做:

SomeType &&Func() { ... }

SomeType &&val = Func();
SomeType otherVal{val};

这是做什么的?在旧版本的规范中,在 5 个值出现之前,这会引起移动。当然可以。您将右值引用传递给构造函数,因此它绑定到采用右值引用的构造函数。这很明显。

这只有一个问题。你没有要求移动它。哦,您可能会说 && 应该是一个线索,但这并不能改变它违反规则的事实。 val 不是临时的,因为临时没有名称。您可能已经延长了临时的生命周期,但这意味着它不是临时的;它就像任何其他堆栈变量一样。

如果它不是临时的,并且你没有要求移动它,那么移动是错误的。

显而易见的解决方案是使 val 成为左值。这意味着你不能离开它。好的;它被命名,所以它是一个左值。

一旦你这样做了,你就不能再说 SomeType&& 在任何地方都意味着同样的事情。您现在已经区分了命名右值引用和未命名右值引用。好吧,命名的右值引用是左值;这就是我们上面的解决方案。那么我们称之为未命名的右值引用(上面 Func 的返回值)是什么?

它不是左值,因为你不能从左值移动。我们需要能够通过返回 && 来移动;你怎么能明确地说要移动一些东西?毕竟,这就是 std::move 返回的内容。它不是右值(旧式),因为它可以在等式的左侧(事情实际上有点复杂,请参阅 this question 和下面的注释)。它既不是左值也不是右值;这是一种新事物。

我们拥有的是一个可以视为左值的值,除了它可以隐式移动。我们称之为 xvalue。

请注意,xvalues 是使我们获得其他两类值的原因:

prvalue 实际上只是前一种右值类型的新名称,即它们是不是 xvalue 的右值。

Glvalues 是一组中的 xvalues 和 lvalues 的联合,因为它们确实共享许多共同的属性。

所以真的,这一切都归结为 xvalues 以及将移动限制在特定位置的需要。这些地方由右值类别定义; prvalues 是隐式移动,xvalues 是显式移动(std::move 返回一个 xvalue)。


这很有趣,但它可以编译吗? Func 不应该有一个返回语句吗?
@Thomas:这是一个例子;它如何创建返回值并不重要。重要的是它返回一个 &&
注意:prvalues 可以在等式的左侧,也可以在 X foo(); foo() = X; 中......由于这个根本原因,我不能完全按照上面的优秀答案进行到底,因为你真的只做新 xvalue 和旧式 prvalue 之间的区别,基于它可以在 lhs 上的事实。
X 是一个类; X foo(); 是一个函数声明,foo() = X(); 是一行代码。 (我在上面的评论中省略了 foo() = X(); 中的第二组括号。)对于我刚刚发布的突出显示此用法的问题,请参阅 stackoverflow.com/questions/15482508/…
@DanNissenbaum“xvalue 不能在赋值表达式的左侧” - 为什么不呢?请参阅ideone.com/wyrxiT
n
nnolte

恕我直言,关于其含义的最佳解释给了我们 Stroustrup + 考虑 Dániel SándorMohan 的示例:

斯特鲁普:

现在我非常担心。显然,我们正走向僵局或一团糟,或两者兼而有之。我花了午餐时间进行分析,看看哪些属性(值)是独立的。只有两个独立的属性:具有身份——即和地址,一个指针,用户可以确定两个副本是否相同,等等。可以从中移动——即我们可以在某些不确定的情况下离开“副本”的来源, 但有效状态 这使我得出结论,恰好有三种值(使用正则表达式符号技巧,使用大写字母表示否定 - 我很着急): iM:有身份,不能从im:具有标识并且可以从其移动(例如,将左值转换为右值引用的结果) Im:没有标识并且可以从中移动。第四种可能性,IM,(没有身份并且不能移动)在 C++(或者,我认为)在任何其他语言中都没有用。除了这三个基本的值分类之外,我们还有两个明显的概括,它们对应于两个独立的属性: i:有身份 m:可以从这让我把这个图放在板上:命名我观察到我们有只有有限的命名自由:左边的两点(标记为 iM 和 i)是或多或少正式的人称之为左值,右边的两点(标记为 m 和 Im)是或多或少正式的人已调用右值。这必须反映在我们的命名中。也就是说,W 的左“腿”应该具有与左值相关的名称,而 W 的右“腿”应该具有与右值相关的名称。我注意到这整个讨论/问题源于右值引用和移动语义的引入。这些概念在 Strachey 仅由右值和左值组成的世界中根本不存在。有人观察到,每个值要么是左值,要么是右值,左值不是右值,右值不是左值的想法深深植根于我们的意识中,非常有用的属性,这种二分法的痕迹遍布整个世界。标准草案。我们都同意我们应该保留这些属性(并使它们精确)。这进一步限制了我们的命名选择。我观察到标准库的措辞使用 rvalue 来表示 m(泛化),因此为了保留标准库的期望和文本,W 的右下点应该命名为 rvalue。这导致了对命名的集中讨论。首先,我们需要决定左值。左值应该表示 iM 还是泛化 i?在 Doug Gregor 的带领下,我们列出了核心语言措辞中左值一词被限定为一个或另一个的地方。列出了一个列表,在大多数情况下,在最棘手/最脆弱的文本中,左值当前意味着 iM。这是左值的经典含义,因为“在过去”没有任何东西被移动; move 是 C++0x 中的一个新概念。此外,命名 W 左值的左上点给我们一个属性,即每个值都是左值或右值,但不是两者兼而有之。因此,W 的左上角是左值,右下角是右值。什么是左下角和右上角的点?左下点是经典左值的泛化,允许移动。所以它是一个广义的左值。我们将其命名为 glvalue。您可以对缩写提出质疑,但(我认为)不符合逻辑。我们假设在认真使用广义左值时无论如何都会以某种方式缩写,所以我们最好立即这样做(或冒着混淆的风险)。 W 的右上角没有右下角那么通用(现在和以往一样,称为右值)。该点代表了您可以从中移动的对象的原始纯概念,因为它不能被再次引用(除了析构函数)。与广义左值相比,我喜欢专门的右值这一短语,但缩写为纯右值的纯右值胜出(而且可能是正确的)。因此,W 的左腿是左值和左值,右腿是右值和右值。顺便说一句,每个值要么是泛左值要么是纯右值,但不是两者兼而有之。这留下了W的顶部中间:im;也就是说,具有标识且可以移动的值。我们真的没有任何东西可以引导我们为那些深奥的野兽起个好名字。它们对使用(草稿)标准文本的人很重要,但不太可能成为家喻户晓的名字。我们在命名上没有发现任何真正的限制来指导我们,所以我们选择了“x”作为中心、未知、奇怪、仅限专家,甚至是 x 级。


是的,如果您想了解它们的含义,最好阅读 C++ 委员会的原始提案和讨论,而不是标准:D
字面量没有身份,不能移出;它们仍然很有用。
我只是想澄清一件事。 int&& f(){ 返回 1; } 和 MyClass&& g(){ return MyClass(); } 返回 xvalue,对吧?那么我在哪里可以找到表达式 f(); 的标识?和“g();”?他们有身份,因为在 return 语句中有另一个表达式,它指的是他们所指的同一个对象——我理解对了吗?
@DrPizza根据标准:字符串文字是lvalue,所有其他文字都是prvalue。严格来说,您可以说非字符串文字应该是不可移动的,但这不是标准的编写方式。
@DrPizza:“可以移动”意味着“可以通过右值引用绑定到”。文字是右值,因为右值引用可以绑定到它们,特别是它们是纯右值,因为它们没有标识。 int &&r = 42; 是有效的,因为表达式 42 是一个右值。没有身份且无法从中移动的表达式是无用的,因为没有指针可以指向它,也没有引用可以绑定到它,因此不可能将它作为参数传递给函数。 (按值传递需要复制构造,这在逻辑上需要绑定对源值的引用。)
D
Dániel Sándor

介绍

ISOC++11(正式名称为 ISO/IEC 14882:2011)是 C++ 编程语言标准的最新版本。它包含一些新功能和概念,例如:

右值引用

xvalue、glvalue、prvalue 表达式值类别

移动语义

如果我们想了解新表达式值类别的概念,我们必须知道存在右值和左值引用。最好知道右值可以传递给非常量右值引用。

int& r_i=7; // compile error
int&& rr_i=7; // OK

如果我们引用工作草案 N3337(与已发布的 ISOC++11 标准最相似的草案)中标题为 Lvalues 和 rvalues 的小节,我们可以对值类别的概念有一些直觉。

3.10 左值和右值 [basic.lval] 1 表达式根据图 1 中的分类法进行分类。左值(历史上称为左值,因为左值可能出现在赋值表达式的左侧)指定函数或对象. [ 示例:如果 E 是指针类型的表达式,则 *E 是一个左值表达式,指的是 E 指向的对象或函数。作为另一个示例,调用返回类型为左值引用的函数的结果是左值。 —end example ] 一个 xvalue(一个“eXpiring”值)也指一个对象,通常接近其生命周期的末尾(例如,它的资源可能会被移动)。 xvalue 是某些涉及右值引用的表达式的结果 (8.3.2)。 [ 示例:调用返回类型为右值引用的函数的结果是 xvalue。 —结束示例] 左值(“广义”左值)是左值或 xvalue。右值(历史上称为右值,因为右值可能出现在赋值表达式的右侧)是一个 xvalue、一个临时对象 (12.2) 或其子对象,或者一个与对象无关的值。 prvalue(“纯”右值)是不是 xvalue 的右值。 [ 示例:调用返回类型不是引用的函数的结果是纯右值。诸如 12、7.3e5 或 true 之类的文字的值也是纯右值。 —end example ] 每个表达式都恰好属于此分类法中的基本分类之一:lvalue、xvalue 或 prvalue。表达式的这个属性称为它的值类别。

但是我不太确定这一小节是否足以清楚地理解这些概念,因为“通常”不是很笼统,“接近其生命周期的尽头”不是很具体,“涉及右值引用”不是很清楚,和“示例:调用返回类型为右值引用的函数的结果是 xvalue。”听起来像是一条蛇在咬它的尾巴。

主要价值类别

每个表达式都恰好属于一个主要值类别。这些值类别是左值、xvalue 和 prvalue 类别。

左值

表达式 E 属于左值类别当且仅当 E 引用一个实体,该实体已经具有使其在 E 之外可访问的身份(地址、名称或别名)。

#include <iostream>

int i=7;

const int& f(){
    return i;
}

int main()
{
    std::cout<<&"www"<<std::endl; // The expression "www" in this row is an lvalue expression, because string literals are arrays and every array has an address.  

    i; // The expression i in this row is an lvalue expression, because it refers to the same entity ...
    i; // ... as the entity the expression i in this row refers to.

    int* p_i=new int(7);
    *p_i; // The expression *p_i in this row is an lvalue expression, because it refers to the same entity ...
    *p_i; // ... as the entity the expression *p_i in this row refers to.

    const int& r_I=7;
    r_I; // The expression r_I in this row is an lvalue expression, because it refers to the same entity ...
    r_I; // ... as the entity the expression r_I in this row refers to.

    f(); // The expression f() in this row is an lvalue expression, because it refers to the same entity ...
    i; // ... as the entity the expression f() in this row refers to.

    return 0;
}

极值

表达式 E 属于 xvalue 范畴当且仅当它是

— 调用函数的结果,无论是隐式还是显式,其返回类型是对所返回对象类型的右值引用,或者

int&& f(){
    return 3;
}

int main()
{
    f(); // The expression f() belongs to the xvalue category, because f() return type is an rvalue reference to object type.

    return 0;
}

— 转换为对对象类型的右值引用,或

int main()
{
    static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs to the xvalue category, because it is a cast to an rvalue reference to object type.
    std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).

    return 0;
}

— 一个类成员访问表达式,指定一个非引用类型的非静态数据成员,其中对象表达式是一个 xvalue,或

struct As
{
    int i;
};

As&& f(){
    return As();
}

int main()
{
    f().i; // The expression f().i belongs to the xvalue category, because As::i is a non-static data member of non-reference type, and the subexpression f() belongs to the xvlaue category.

    return 0;
}

— 指向成员的表达式,其中第一个操作数是 xvalue,第二个操作数是指向数据成员的指针。

请注意,上述规则的效果是,对对象的命名右值引用被视为左值,对对象的未命名右值引用被视为 xvalue;对函数的右值引用被视为左值,无论是否命名。

#include <functional>

struct As
{
    int i;
};

As&& f(){
    return As();
}

int main()
{
    f(); // The expression f() belongs to the xvalue category, because it refers to an unnamed rvalue reference to object.
    As&& rr_a=As();
    rr_a; // The expression rr_a belongs to the lvalue category, because it refers to a named rvalue reference to object.
    std::ref(f); // The expression std::ref(f) belongs to the lvalue category, because it refers to an rvalue reference to function.

    return 0;
}

prvalues

当且仅当 E 既不属于左值也不属于 xvalue 范畴时,表达式 E 属于纯右值范畴。

struct As
{
    void f(){
        this; // The expression this is a prvalue expression. Note, that the expression this is not a variable.
    }
};

As f(){
    return As();
}

int main()
{
    f(); // The expression f() belongs to the prvalue category, because it belongs neither to the lvalue nor to the xvalue category.

    return 0;
}

混合价值类别

还有两个更重要的混合价值类别。这些值类别是右值和左值类别。

右值

当且仅当 E 属于 xvalue 类别或 prvalue 类别时,表达式 E 属于 rvalue 类别。

请注意,此定义意味着表达式 E 属于右值类别当且仅当 E 引用的实体没有任何身份使其可以在 E YET 之外访问。

左值

当且仅当 E 属于左值类别或 xvalue 类别时,表达式 E 属于左值类别。

实用规则

Scott Meyer published 有一个非常有用的经验法则来区分右值和左值。

如果您可以获取表达式的地址,则该表达式是左值。如果表达式的类型是左值引用(例如,T& 或 const T& 等),则该表达式是左值。否则,表达式是一个右值。从概念上(通常实际上也是),右值对应于临时对象,例如从函数返回或通过隐式类型转换创建的对象。大多数文字值(例如,10 和 5.3)也是右值。


左值的所有示例和 xvalue 的所有示例也是泛左值的示例。感谢您的编辑!
你说的对。三个主要的价值类别就足够了。右值也不是必需的。我认为 rvalue 和 glvalue 在标准中是为了方便。
很难理解 struct As{void f(){this;}} this 变量是纯右值。我认为 this 应该是一个左值。直到标准 9.3.2 说:在非静态(9.3)成员函数的主体中,关键字 this 是一个纯右值表达式。
@r0ng this 是纯右值,但 *this 是左值
"www" doesn't always 具有相同的地址。它是一个左值 because it is an array
F
Felix Dombek

我为此苦苦挣扎了很长时间,直到遇到 cppreference.com 对 value categories 的解释。

它实际上相当简单,但我发现它经常以难以记忆的方式解释。这里非常示意性地解释了它。我将引用页面的某些部分:

主要类别 主要值类别对应于表达式的两个属性: 具有同一性:可以确定表达式是否与另一个表达式引用相同的实体,例如通过比较对象的地址或它们标识的函数(直接或间接获得) );可以从:移动构造函数、移动赋值运算符或实现移动语义的另一个函数重载可以绑定到表达式。具有同一性且不能从中移动的表达式称为左值表达式;具有身份并且可以从中移动的称为 xvalue 表达式;没有身份并且可以移动的称为prvalue表达式;没有身份,不能从不使用。左值 左值(“左值”)表达式是具有标识且不能从中移动的表达式。右值(C++11 前)、纯右值(C++11 起) 纯右值(“纯右值”)表达式是不具有标识且可以从中移动的表达式。 xvalue xvalue(“到期值”)表达式是具有标识并且可以从中移动的表达式。 glvalue 一个glvalue(“广义左值”)表达式是一个左值或xvalue的表达式。它有身份。它可能会或可能不会被移动。右值(C++11 起) 右值(“右值”)表达式是纯右值或 xvalue 的表达式。可以从中移出。它可能有也可能没有身份。

所以让我们把它放到一个表中:

可以从 (= rvalue) 移动 不能从 移动 有标识 (= glvalue) xvalue lvalue 没有标识 prvalue 未使用


在某些书籍中,xvalues 的 x 来自“专家”或“特殊”
更重要的是,他们全面的示例列表。
Kris van Res 在他的演讲中解释了这张桌子:youtu.be/km3Urog_wzk?t=868
J
Johannes Schaub - litb

C++03 的类别过于受限,无法正确地将右值引用引入到表达式属性中。

随着它们的引入,据说一个未命名的右值引用评估为一个右值,这样重载决议会更喜欢右值引用绑定,这将使它选择移动构造函数而不是复制构造函数。但发现这会导致各种问题,例如 Dynamic Types 和资格。

为了证明这一点,考虑

int const&& f();

int main() {
  int &&i = f(); // disgusting!
}

在 xvalue 之前的草稿中,这是允许的,因为在 C++03 中,非类类型的右值永远不是 cv 限定的。但是 const 旨在应用于右值引用的情况,因为这里我们确实引用了对象(=内存!),并且从非类右值中删除 const 主要是因为周围没有物体。

动态类型的问题具有相似的性质。在 C++03 中,类类型的右值具有已知的动态类型——它是该表达式的静态类型。因为要以另一种方式使用它,您需要引用或取消引用,它们评估为左值。未命名的右值引用并非如此,但它们可以显示多态行为。所以要解决它,

未命名的右值引用成为 xvalues。它们可以是合格的,并且可能具有不同的动态类型。它们确实像预期的那样在重载期间更喜欢右值引用,并且不会绑定到非常量左值引用。

以前是右值(文字,由强制转换为非引用类型创建的对象)现在变成了纯右值。它们在重载期间具有与 xvalues 相同的偏好。

以前是左值的东西仍然是左值。

并且进行了两个分组来捕获那些可以限定并且可以具有不同动态类型(glvalues)的分组,以及那些重载更喜欢右值引用绑定(rvalues)的分组。


答案显然是合理的。 xvalue 只是右值,它可以是 cv 限定的和动态类型的!
t
thebrandre

由于前面的答案详尽地涵盖了价值类别背后的理论,我还想补充一件事:您可以实际使用它并对其进行测试。

对于值类别的一些动手实验,您可以使用 decltype specifier。它的行为明确区分了三个主要值类别(xvalue、lvalue 和 prvalue)。

使用预处理器为我们节省了一些打字...

主要类别:

#define IS_XVALUE(X) std::is_rvalue_reference<decltype((X))>::value
#define IS_LVALUE(X) std::is_lvalue_reference<decltype((X))>::value
#define IS_PRVALUE(X) !std::is_reference<decltype((X))>::value

混合类别:

#define IS_GLVALUE(X) (IS_LVALUE(X) || IS_XVALUE(X))
#define IS_RVALUE(X) (IS_PRVALUE(X) || IS_XVALUE(X))

现在我们可以(几乎)重现 cppreference on value category 中的所有示例。

以下是 C++17 的一些示例(用于简洁的 static_assert):

void doesNothing(){}
struct S
{
    int x{0};
};
int x = 1;
int y = 2;
S s;

static_assert(IS_LVALUE(x));
static_assert(IS_LVALUE(x+=y));
static_assert(IS_LVALUE("Hello world!"));
static_assert(IS_LVALUE(++x));

static_assert(IS_PRVALUE(1));
static_assert(IS_PRVALUE(x++));
static_assert(IS_PRVALUE(static_cast<double>(x)));
static_assert(IS_PRVALUE(std::string{}));
static_assert(IS_PRVALUE(throw std::exception()));
static_assert(IS_PRVALUE(doesNothing()));

static_assert(IS_XVALUE(std::move(s)));
// The next one doesn't work in gcc 8.2 but in gcc 9.1. Clang 7.0.0 and msvc 19.16 are doing fine.
static_assert(IS_XVALUE(S().x)); 

一旦你弄清楚了主要类别,混合类别就有点无聊了。

如需更多示例(和实验),请查看以下link on compiler explorer。不过,不要费心阅读程序集。我添加了很多编译器只是为了确保它适用于所有常见的编译器。


我认为 #define IS_GLVALUE(X) IS_LVALUE(X) || IS_XVALUE(X) 实际上应该是 #define IS_GLVALUE(X) (IS_LVALUE(X) || IS_XVALUE(X)) 否则看看如果你 && 两个 IS_GLVALUE 会发生什么。
W
Winter

这些新类别与现有的右值和左值类别有何关系?

C++03 左值仍然是 C++11 左值,而 C++03 右值在 C++11 中称为右值。


0
0xF

这些是 C++ 委员会用来在 C++11 中定义移动语义的术语。 Here's the story

鉴于它们的精确定义,long lists of rules 或这个流行的图表,我发现很难理解这些术语:

https://i.stack.imgur.com/GNhBF.png

在带有典型示例的维恩图上更容易:

https://i.stack.imgur.com/xgCnV.png

基本上:

每个表达式都是左值或右值

lvalue必须复制,因为它有标识,所以以后可以使用

右值可以移动,因为它是临时的(prvalue)或显式移动的(xvalue)

现在,好问题是,如果我们有两个正交属性(“具有身份”和“可以移动”),完成左值、xvalue 和 prvalue 的第四个类别是什么?那将是一个没有身份的表达式(因此以后无法访问)并且无法移动(需要复制其值)。这根本没用,所以没有命名。


这很清楚,是我见过的唯一明确的答案。是的,我的经历完全符合您的描述;特别是,流行的图表对于了解正在发生的事情没有帮助,因为除非人们已经理解了这些关系并且本质上在他们的脑海中拥有维恩图,否则它是没有意义的。
好的。我将“必须复制左值,因为它具有身份”这一子句更改为“不得移动左值......”,更准确地说(实际上更少误导),并删除“因为它具有身份”部分,因为这似乎不正确:xvalues 也有身份,但它们没有被复制,而是被移动。 (不幸的是,与大多数解释所暗示的相比,拥有身份是一个不那么直观的特征。)然后我会赞成。
M
Mohan

上面优秀答案的一个附录,即使在我阅读了 Stroustrup 并认为我理解了右值/左值的区别之后,这一点也让我感到困惑。当你看到

int&& a = 3

int&& 视为一种类型并得出结论 a 是一个右值是非常诱人的。它不是:

int&& a = 3;
int&& c = a; //error: cannot bind 'int' lvalue to 'int&&'
int& b = a; //compiles

a 有一个名称,并且事实上是一个左值。 不要将 && 视为 a 类型的一部分;它只是告诉您允许绑定 a 的内容。

这对于构造函数中的 T&& 类型参数尤其重要。如果你写

Foo::Foo(T&& _t) : t{_t} {}

您将 _t 复制到 t。你需要

Foo::Foo(T&& _t) : t{std::move(_t)} {} 如果您想移动。当我遗漏 move 时,我的编译器会警告我吗?


我认为这个答案可以澄清。 “允许绑定什么 a”:当然,但在第 2 行 & 3 你的变量是 c & b,它不是绑定到的 a,a 的类型在这里是无关紧要的,不是吗?如果将 a 声明为 int a,那么这些行将是相同的。此处实际的主要区别在于,在第 1 行中,a 不必为 const 即可绑定到 3。
D
Daniel Russell

这是我为我正在编写的一本高度可视化的 C++ 书籍制作的维恩图,我将在开发期间很快在leapub 上发布。

https://i.stack.imgur.com/e6xMb.png

其他答案用文字更详细,并显示类似的图表。但希望这个信息的呈现是相当完整的,并且对参考有用,此外。

我对这个主题的主要收获是表达式具有这两个属性:身份和可移动性。第一个涉及事物存在的“坚固性”。这很重要,因为允许并鼓励 C++ 抽象机通过优化来积极地更改和缩小代码,这意味着没有身份的东西可能只存在于编译器的脑海中或在被践踏之前的短暂的寄存器中上。但是如果你回收它的内脏,这样的数据也保证不会引起问题,因为没有办法尝试使用它。因此,发明了移动语义以允许我们捕获对临时对象的引用,将它们升级为左值并延长它们的生命周期。

移动语义最初不仅仅是浪费地扔掉临时的东西,而是把它们送给别人,这样它们就可以被另一个人消耗掉。

https://i.stack.imgur.com/aDlzx.png

当你把你的玉米面包送出去时,你送给它的人现在拥有它。他们消费它。一旦你把它送出去,你不应该尝试吃或消化所说的玉米面包。也许那个玉米面包无论如何都被扔进了垃圾箱,但现在它却被扔到了他们的肚子里。它不再是你的了。

在 C++ 领域,“消耗”资源的想法意味着资源现在归我们所有,因此我们应该进行必要的清理,并确保不会在其他地方访问该对象。通常,这意味着借用胆量来创建新对象。我称之为“捐赠器官”。通常,我们谈论的是对象包含的指针或引用,或类似的东西,我们希望保留这些指针或引用,因为它们引用了我们程序中其他地方没有死亡的数据。

因此,您可以编写一个接受右值引用的函数重载,如果传入了一个临时(prvalue),那就是要调用的重载。在绑定到函数采用的右值引用时,将创建一个新的左值,从而延长临时变量的寿命,以便您可以在函数中使用它。

在某个时候,我们意识到我们经常在一个范围内完成了左值非临时数据,但想在另一个范围内蚕食。但它们不是右值,因此不会绑定到右值引用。所以我们制作了 std::move,它只是一个从左值到右值引用的花哨转换。这样的数据是一个 xvalue:一个以前的左值现在就像一个临时的,所以它也可以被移动。


考虑将内容复制为文本,for reasons outlined here。关于 xvalues:std::move 的结果是 xvalue,尽管它所指的对象不一定会很快消亡。
不幸的是,这会破坏这张图的意义,因为我的大脑需要一张图表而不是一堵文字墙,所以多年来(有点)知道它,我一直在努力记住和处理这些信息。但你是对的,如果这是主要答案,它应该更加面向文本。我希望这个答案没问题。只是希望能帮助别人。