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

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

右值左值x值glvalue值prvalue值

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

这些新的表达类别是什么?这些新类别与现有的右值和左值类别有何关联?C++0x中的右值和左值类别是否与C++03中的相同?为什么需要这些新类别?WG21众神只是想迷惑我们这些凡人吗?


当前回答

引言

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 L值和R值[基本值]1表达式根据图1中的分类法进行分类。左值(历史上称为左值,因为左值可能出现在赋值表达式的左侧)表示函数或物体。[示例:如果E是指针类型的表达式,则*E是指E指向的对象或函数的左值表达式。作为另一个示例,调用函数的结果其返回类型为左值引用的值为左值-结束示例]xvalue(“eXpiring”值)也指的是一个对象,通常在其生命周期结束时(这样它的资源就可以移动,例如示例)。xvalue是某些类型表达式的结果涉及右值引用(8.3.2)。[示例:调用返回类型为右值引用的函数为xvalue-终止示例]glvalue(“广义”左值)是左值或x值。右值(历史上称为,因为右值可能出现在赋值表达式的右侧)是一个xvalue,一个临时对象(12.2)或其子对象,或不是与对象关联。prvalue(“纯”右值)是一个不是xvalue的右值。[示例:调用返回类型不是引用是prvalue。文字的值,例如12、7.3e5或true也是prvalue-结束示例]每个表达式都属于此分类法中的分类:lvalue、xvalue或prvalue。这表达式的属性称为其值类别。

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

主要价值类别

每个表达式都属于一个主值类别。这些值类别是lvalue、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;
}

x值

表达式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;
}

pr值

表达式E属于prvalue类别,当且仅当E既不属于lvalue类别,也不属于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;
}

混合价值类别

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

右值

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

请注意,此定义意味着表达式E属于右值类别,当且仅当E指的是一个实体,该实体没有任何使其在E YET之外可访问的标识。

gl值

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

实用规则

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

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

其他回答

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

其他答案用文字更详细,并显示类似的图表。但希望这些信息的介绍是相当完整的,并且对参考也很有用。

在这个主题上,我的主要收获是表达式具有这两个财产:身份和可动性。第一个涉及事物存在的“坚固性”。这一点很重要,因为C++抽象机被允许并鼓励通过优化来积极地更改和压缩代码,这意味着没有身份的东西可能只会在编译器或寄存器中存在片刻,然后才会被践踏。但是,如果你回收它的内部,这样的数据也保证不会引起问题,因为没有办法尝试使用它。因此,移动语义被发明出来,允许我们捕获对临时变量的引用,将其升级为lvalues并延长其寿命。

移动语义最初不仅仅是为了浪费时间,而是为了让它们可以被其他人使用。

当你把你的玉米面包送人时,你送人的人现在拥有它。他们会吃掉它。一旦你送人,你不应该试图吃或消化这些玉米面包。也许玉米面包本来是往垃圾堆里去的,但现在是往他们的肚子里去了。它不再是你的了。

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

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

在某一时刻,我们意识到,我们经常会在一个范围内处理完左值非临时数据,但却希望在另一个范围中进行分解。但它们不是右值,因此不会绑定到右值引用。所以我们做了std::move,这只是一个从左值到右值引用的花式转换。这样的数据是一个xvalue:一个以前的lvalue,现在就像它是一个临时的,所以它也可以从中移动。

由于前面的答案详尽地涵盖了价值类别背后的理论,我想补充一点:你可以实际使用并测试它。

对于值类别的一些实际实验,可以使用decltype说明符。它的行为明确区分了三个主要值类别(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中的所有示例。

以下是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)); 

一旦你确定了主要类别,混合类别就有点无聊了。

有关更多示例(和实验),请查看编译器资源管理器上的以下链接。不过,不要费心阅读汇编。我添加了很多编译器,只是为了确保它能在所有常见的编译器中运行。

我一直在纠结这个问题,直到我看到cpreference.com对价值类别的解释。

这其实很简单,但我发现它的解释方式很难记住。这里非常示意性地解释了这一点。我将引用页面的某些部分:

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

让我们把它放到一张桌子上:

Can be moved from (= rvalue) Cannot be moved from
Has identity (= glvalue) xvalue lvalue
No identity prvalue not used

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

为什么需要这些新类别?

C++标准包含许多处理表达式的值类别的规则。有些规则区分左值和右值。例如,当涉及到过载解决方案时。其他规则区分glvalue和prvalue。例如,可以有一个glvalue具有不完整或抽象类型,但没有prvalue具有不完全或抽象类型。在我们使用这个术语之前,实际上需要区分glvalue/prvalue的规则称为lvalue/rvalue,它们要么是无意中错误的,要么包含大量对规则的解释和例外“……除非rvalue是由于未命名的rvalue引用……”。因此,只给glvalues和prvalues的概念起自己的名字似乎是个好主意。

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

我们仍然有与C++98兼容的术语lvalue和rvalue。我们将右值分为两个子组,xvalue和prvalue,我们将左值和xvalue称为glvalue。Xvalues是未命名右值引用的一种新的值类别。每个表达式都是这三个值之一:lvalue、xvalue、prvalue。Venn图如下所示:

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

函数示例:

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

但也不要忘记,命名的rvalue引用是lvalues:

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

C++03的类别太有限,无法将右值引用正确引入表达式属性。

随着它们的引入,据说未命名的右值引用求值为右值,因此重载解析将倾向于右值引用绑定,这将使其选择移动构造函数而不是复制构造函数。但研究发现,这会导致各种问题,例如动态类型和限定。

要显示这一点,请考虑

int const&& f();

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

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

动态类型的问题具有相似的性质。在C++03中,类类型的右值有一个已知的动态类型——它是该表达式的静态类型。因为要以另一种方式实现,您需要引用或解引用,它们的计算结果为左值。对于未命名的右值引用,情况并非如此,但它们可以显示多态行为。为了解决这个问题,

未命名的右值引用变为xvalue。它们可以是限定的,并且可能具有不同的动态类型。它们确实像预期的那样,在重载期间更喜欢右值引用,并且不会绑定到非常值左值引用。以前是一个rvalue(文本,通过对非引用类型的强制转换创建的对象)现在变成了一个prvalue。在重载期间,它们与xvalue具有相同的偏好。以前的左值仍然是左值。

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