c++ - 左右值 - 什么是右值,左值,xvalues,glvalues和prvalues?




rvalue reference (8)

How do these new categories relate to the existing rvalue and lvalue categories?

A C++03 lvalue is still a C++11 lvalue, whereas a C++03 rvalue is called a prvalue in C++11.

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

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

  1. 右值
  2. 左值
  3. x值
  4. glvalue
  5. prvalue

两类分为五类。

  • 这些新的表达类别是什么?
  • 这些新类别如何与现有的右值和左值类别相关联?
  • C ++ 0x中的右值和左值类别与C ++ 03中的相同吗?
  • 为什么需要这些新类别? WG21神只是试图将我们这些凡人混为一谈?

为什么需要这些新类别? WG21神只是试图将我们这些凡人混为一谈?

我不觉得其他答案(虽然其中很多答案都很好)确实捕捉了这个特定问题的答案。 是的,这些类别和类别存在允许移动语义,但出于某种原因存在复杂性。 这是在C ++ 11中移动东西的一个不可侵犯的规则:

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

这就是为什么存在这些类别的原因:能够谈论可以安全转移的价值观,以及在不存在价值的地方谈论价值。

在最早版本的r值引用中,移动很容易发生。 容易了。 很容易,当用户没有真正的意图时,隐含地移动事物有很大的潜力。

以下是可以安全移动某些东西的环境:

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

如果你这样做:

SomeType &&Func() { ... }

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

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

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

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

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

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

这不是一个左值,因为你不能从左值移动。 我们需要能够通过返回&&来移动; 你还可以明确地说要搬家吗? 毕竟,这就是std::move结果。 这不是一个右值(旧式),因为它可能在方程的左边(事实上有点复杂,请参阅下面的问题和评论)。 它既不是左值也不是右值; 这是一种新的东西。

我们拥有的是一种价值,你可以把它看作是一个左值, 只是它可以隐式地从中移除 。 我们称之为一个xvalue

请注意,xvalues是我们获得另外两类值的原因:

  • 一个prvalue实际上只是前一类右值的新名称,即它们是不是 xvalues的右值。

  • glvalues是一个组中的xvalue和lvalues的结合 ,因为它们共享很多共同的属性。

所以真的,这一切都归结为xvalues,并且需要将运动限制在恰好和特定的地方。 这些地方由右值类别定义; prvalues是隐式的动作,而xvalues是明确的动作( std::move返回一个xvalue)。


介绍

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

  • 右值引用
  • xvalue,glvalue,prvalue表达式值类别
  • 移动语义

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

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指向的对象或函数的左值表达式。 作为另一个例子,调用返回类型是左值引用的函数的结果是左值。 - 例子]
  • xvalue(“eXpiring”值)也指对象,通常接近其生命周期结束时(例如,可以移动其资源)。 xvalue是涉及右值引用(8.3.2)的某些表达式的结果。 [例子:调用返回类型是右值引用的函数的结果是一个xvalue。 - 例子]
  • 一个glvalue(“广义”左值)是一个左值或一个xvalue。
  • 一个右值(所谓的,历史上,因为右值可能出现在赋值表达式的右侧)是一个xvalue,一个
    临时对象(12.2)或其子对象,或者不是的值
    与一个对象相关联。
  • 一个prvalue(“纯”右值)是一个不是xvalue的右值。 [例子:调用返回类型不是的函数的结果
    参考是一个值。 一个文字的值,如12,7.3e5或
    真实也是一种价值。 - 例子]

每个表达式都属于该分类中的基本分类之一:左值,左值或右值。 表达式的这个属性被称为它的值类别。

但我不太清楚这一小节是否足以清楚地理解这些概念,因为“通常”并不是一般的,“接近其一生的终点”并不是真正具体的,“涉及右值引用”并不十分清楚,和“示例:调用返回类型为右值引用的函数的结果是一个xvalue。” 听起来像是一条蛇咬着它的尾巴。

主要价值类别

每个表达式都只属于一个主要值类别。 这些值类别是左值,左值和右值类别。

左值

表达式E属于左值范畴,当且仅当E引用实体ALREADY有一个身份(地址,名称或别名),使它可以在E之外访问。

#include <iostream>

int i=7;

const int& f(){
    return i;
}

int main()
{
    std::cout<<&"www"<<std::endl; // This address ...
    std::cout<<&"www"<<std::endl; // ... and this address are the same.
    "www"; // The expression "www" in this row is an lvalue expression, because it refers to the same entity ...
    "www"; // ... as the entity the expression "www" in this row refers to.

    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;
}

xvalues

表达式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,第二个操作数是指向数据成员的指针。

请注意,上述规则的效果是,对象的名称rvalue引用被视为左值,而对对象的未命名右值引用则被视为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属于prvalue类,当且仅当E既不属于左值也不属于xvalue类。

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;
}

混合价值类别

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

右值

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

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

glvalues

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

实用规则

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

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

C ++ 03的类别太受限制,无法正确地将rvalue引用引入到表达式属性中。

通过引入它们,据说一个未命名的右值引用的值为右值,这样重载解析会更喜欢右值引用绑定,这将使其选择移动构造函数而不是复制构造函数。 但发现这会导致所有问题,例如动态类型和资格。

为了表明这一点,请考虑

int const&& f();

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

在pre-xvalue草稿上,这是允许的,因为在C ++ 03中,非类类型的rvalues永远不会被cv限定。 但是它的目的是const在右值引用的情况下应用,因为在这里我们引用了对象(= memory!),并且从非类右值中删除const主要是因为没有对象。

动态类型的问题具有类似的性质。 在C ++ 03中,类类型的右值具有已知的动态类型 - 它是该表达式的静态类型。 因为有另一种方式,你需要引用或取消引用,评估为左值。 对于未命名的右值引用,这是不正确的,但它们可以显示多态行为。 所以要解决它,

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

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

  • 以前是一个左值留下一个左值。

还有两个分组用于捕获那些可以被限定的,可以有不同的动态类型( glvalues )和那些重载比较喜欢右值引用绑定( rvalues )的类型。


One addendum to the excellent answers above, on a point that confused me even after I had read Stroustrup and thought I understood the rvalue/lvalue distinction. When you see

int&& a = 3 ,

it's very tempting to read the int&& as a type and conclude that a is an rvalue. It's not:

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

a has a name and is ipso facto an lvalue. Don't think of the && as part of the type of a ; it's just something telling you what a is allowed to bind to.

This matters particularly for T&& type arguments in constructors. If you write

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

you will copy _t into t . 你需要

Foo::Foo(T&& _t) : t{std::move(_t)} {} if you want to move. Would that my compiler warned me when I left out the move !


恕我直言,关于它的含义的最好解释给了我们stroustrup.com/terminology.pdf +考虑到DánielSándor和Mohan例子:

斯特劳斯:

现在我非常担心。 显然,我们正在陷入僵局或混乱或两者兼而有之。 我花了午餐时间做了分析,看看哪些属性(值)是独立的。 只有两个独立的属性:

  • has identity (即,地址)指针,用户可以确定两个副本是否相同,等等。
  • can be moved from - 即我们被允许以一些不确定但有效的状态离开到“复制”的来源

这使我得出结论,确切地说有三种价值观(使用正则表达式使用大写字母表示否定的标记技巧 - 我很着急):

  • iM :有身份,不能从中移出
  • im :具有身份并且可以从(例如将左值转换为右值引用的结果)
  • Im :没有身份,可以从第四种可能性( IM :没有身份并且无法移动)在C++ (或者我认为)中用于任何其他语言。

除了这三个基本的价值分类之外,我们还有两个明显的概括,它们与两个独立的属性相对应:

  • i :有身份
  • m :可以从中移出

这让我把这个图表放在板子上:

命名

我观察到,我们的名字只有有限的自由度:左边的两个点(标记为iMi )是指具有或多或少形式化的人称为左值,右边的两个点(标记为mIm )是人们或多或少的形式称为rvalues 。 这必须反映在我们的命名中。 也就是说, W的左边“腿”应该有与lvalue相关的名字,而W的右边“腿”应该有与rvalue.相关的名字rvalue. 我注意到,整个讨论/问题来自于引用右值引用和移动语义。 这些概念根本不存在于由rvaluesrvalues组成的rvalues世界中。 有人观察到这样的想法

  • 每个value都是lvaluervalue
  • lvalue不是rvaluervalue不是lvalue

深深植根于我们的意识中,非常有用的特性,这种二分法的痕迹可以在草案标准中找到。 我们都同意我们应该保留这些属性(并且使它们精确)。 这进一步限制了我们的命名选择。 我观察到标准库的措辞使用rvalue来表示m (泛化),为了保留标准库的期望和文本, W右下角应该被命名为rvalue.

这导致了对命名的集中讨论。 首先,我们需要决定lvalue. 应该lvalue意味着iM还是泛化? 在Doug Gregor的领导下,我们在核心语言文字中列出了lvalue合格的意思。 列表是在大多数情况下做出的,最棘手/脆弱的文本lvalue表示iM 。 这是左值的古典意义,因为“在旧时代”没有什么东西被移动; moveC++0x一种新概念。 此外,命名W lvalue的最小值点给了我们属性,即每个值都是lvaluervalue ,但不是两者。

所以, W左上角是lvalue ,右下角是rvalue. 这是什么使左下角和右上角的点? 左下角是经典左值的推广,允许移动。 所以这是一个generalized lvalue. 我们把它命名为glvalue. 你可以质疑这个缩写,但是(我认为)不是用逻辑。 我们认为,在严重使用中, generalized lvalue无论如何都会以某种方式缩写,所以我们最好立即做(或者冒险)。 W的右上角的点比右下角的一般点(现在,一直称为rvalue )。 这一点代表了你可以移动的对象的原始纯粹概念,因为它不能再被引用(除了析构函数)。 我喜欢短语specialized rvalue generalized lvalue而不是generalized lvalue但是简单的pure rvalue胜出(也许是正确的)。 所以,W的左腿是lvalue和右lvalueglvaluelvalueprvalue rvalue. 顺便说一句,每个值都是一个值或一个值,但不是两个值。

这留下了Wim的顶部中间; 也就是说,具有身份并且可以移动的值。 我们真的没有任何东西能够引导我们为这些神秘的野兽命名。 它们对使用(草案)标准文本的人很重要,但不可能成为家喻户晓的名字。 我们没有发现任何真正的命名限制来指导我们,所以我们选择了'x'作为中心,未知,奇怪,只有xpert,甚至是x-rated。


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

为什么需要这些新类别?

C ++标准包含许多处理表达式值类别的规则。 一些规则区分了左值和右值。 例如,涉及重载分辨率。 其他规则区分了glvalue和prvalue。 例如,可以使用不完整或抽象类型的glvalue,但没有不完整或抽象类型的prvalue。 在我们使用这个术语之前,实际上需要区分glvalue / prevalue是指左值/右值的规则,它们或者是无意的错误,或者是包含了很多解释和例外规则,a“......除非右值是由于未命名右值引用...“。 所以,给予他们自己的名字的价值和价值的概念似乎是一个好主意。

这些新的表达类别是什么? 这些新类别如何与现有的右值和左值类别相关联?

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

    ______ ______
   /      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
}

我想这个文件可能不是那么简短的介绍: n3055

整个屠杀始于移动语义。 一旦我们有了可以移动但不被复制的表达式,突然容易掌握的规则就要求区分可以移动的表达式和在哪个方向上。

根据我对草案的看法,r / l值的区别保持不变,只是在移动东西时变得混乱。

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

引用n3055

  • 一个左值 (所谓的,在历史上,因为左值可以出现在赋值表达式的左边)指定一个函数或一个对象。 [例如:如果E是指针类型的表达式,则*E是指向*E指向的对象或函数的左值表达式。 作为另一个例子,调用返回类型为左值引用的函数的结果是一个左值。]
  • xvalue (“eXpiring”值)也指对象,通常接近其生命周期结束时(例如,可以移动其资源)。 xvalue是涉及右值引用的某些表达式的结果。 [例子:调用返回类型是右值引用的函数的结果是一个xvalue。]
  • 一个glvalue (“广义”左值)是一个左值或一个xvalue
  • 一个右值 (所谓的,历史上,因为右值可能出现在赋值表达式的右侧)是一个xvalue,它的一个临时对象或子对象,或一个与对象无关的值。
  • 一个prvalue (“纯”右值)是一个不是xvalue的右值。 [例子:调用返回类型不是引用的函数的结果是一个prvalue]

所讨论的文件对于这个问题来说是一个很好的参考,因为它显示了标准中引入新术语后发生的确切变化。







c++11