7.2.1 Value category

左值常见形式以及特性 点击查看更多
  • 变量,函数,模板参数对象(c++20起)或数据成员的名称,无论类型如何。即使变量的类型是右值引用,由其名称组成的表达式仍是左值表达式
1
2
3
4
5
6
7
8
void foo() {}

void baz()
{
// `foo` is lvalue
// address may be taken by built-in address-of operator
void (*p)() = &foo;
}
1
2
3
4
5
6
7
struct foo {};

template <foo a>
void baz()
{
const foo* obj = &a; // `a` is an lvalue, template parameter object
}
  • 函数调用或重载运算符表达式,其返回值类型为左值引用
1
2
3
4
5
6
7
8
9
10
int& a_ref()
{
static int a{3};
return a;
}

void foo()
{
a_ref() = 5; // `a_ref()` is lvalue, function call whose return type is lvalue reference
}
  • a = b,a += b,a %= b以及所有其他内置赋值和复合赋值表达式;
  • ++a--a,内置的前置自增和自减表达式;
  • *p,内置的解引用表达式;
  • a[n]p[n](对数组或指针执行下标操作),内置的下标表达式,其中a[n]的一个操作数(a)是数组左值(自 C++11 起);
  • a.m,对象成员访问表达式,(在以下情况下a.m不是左值:①m是成员枚举器(本质是编译期常量?纯右值)②或m是非静态成员函数(纯右值)③或a是右值且m是对象类型的非静态数据成员的情况(此时a.m是将亡值));
1
2
3
4
5
6
7
8
9
10
11
12
13
struct foo
{
enum bar
{
m // member enumerator
};
};

void baz()
{
foo a;
a.m = 42; // ill-formed,赋值左操作数必须得是左值
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct foo
{
void m() {} // non-static member function
};

void baz()
{
foo a;

// a.m is a prvalue, 不能用内建操作符 & 取地址
void (foo::*p1)() = &a.m; // ill-formed

void (foo::*p2)() = &foo::m; // OK: pointer to member function
}
1
2
3
4
5
6
7
8
9
10
11
struct foo
{
static void m() {} // static member function
};

void baz()
{
foo a;
void (*p1)() = &a.m; // a.m is an lvalue
void (*p2)() = &foo::m; // the same
}
  • p->m,内置的成员指针的表达式(在以下情况下p->m不是左值:①m是成员枚举器②m是非静态成员函数);
  • a.*mp,通过成员指针访问对象成员的表达式,其中a是左值,mp是指向数据成员的指针;
  • p->*mp,内置的指向对象成员的指针表达式,其中mp是指向数据成员的指针;
  • a, b,内置的逗号表达式,其中b是左值;
  • a ? b : c,针对特定bc的三元表达式(例如,它们都是相同类型的左值,但请参考定义中的详细说明);
  • 一个字符串字面量,例如”Hello, world!”;
  • 一个转换为左值引用类型的类型转换表达式,例如static_cast<int&>(x)static_cast<void(&)(int)>(x);
  • 一个左值引用类型的常量模板参数;
1
2
3
4
5
6
7
8
9
10
11
12
template <int& v>
void set()
{
v = 5; // template parameter is lvalue
}

int a{3}; // static variable, fixed address is known at compile-time

void foo()
{
set<a>();
}
  • 函数调用或重载运算符表达式,其返回类型为函数的右值引用。(c++11起)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 定义一个函数类型
using func_t = void(int);

// 一个普通函数
void foo(int x)
{
std::cout << "foo called with " << x << "\n";
}

// 定义一个返回“对函数的右值引用”的函数
func_t&& make_func()
{
// 返回一个现有函数的引用
return static_cast<func_t&&>(foo);
}

int main()
{
// 调用make_func()的结果是一个“函数的右值引用”
// 它本身是一个左值
make_func()(42);
}
  • 一个转换为的函数类型的右值引用的类型转换表达式,例如static_cast<void(&&)(int)>(x)(c++11起)

特性:

  • 具有与泛左值相同的特性
  • 左值的地址可以通过内置的取地址运算符获取:&++i&std::hex都是有效的表达式
  • 一个可修改的左值可以用作内置赋值运算符和复合赋值运算符的左操作数
  • 左值可以用来初始化左值引用;这会为标识的对象关联一个新的名称
纯右值常见形式以及特性 点击查看更多
  • 字面量(除了字符串字面量),例如42truenullptr
  • 函数调用或重载运算符表达式,其返回类型不是引用。例如str.substr(1,2)str1 + str2it++
  • a++a--,内置的后置自增自减表达式
  • a + ba % ba & ba << b以及所有其他内置的算术表达式;
  • a && ba || b!a,内置的逻辑表达式;
  • a < ba == ba >= b以及所有其他内置的比较表达式;
  • &a,内置的取地址表达式;
  • a.m,对象成员访问表达式。其中m得是成员枚举器或非静态成员函数。
  • p->m,内置的成员指针的表达式,其中m得是成员枚举器或非静态成员函数
  • a.*mp,通过成员指针访问对象成员的表达式,其中mp是成员函数的指针;
  • p->*mp,内置的指向对象成员的指针表达式,其中mp是成员函数的指针;
  • a, b,内置的逗号表达式,其中b是纯右值;
  • a ? b : c,针对特定bc的三元表达式
  • 一个转换为非引用类型的类型转换表达式,例如static_cast<double>(x)std::string{}(int)42
  • this指针
  • 枚举值
  • 一个标量类型的常量模板参数
1
2
3
4
5
6
7
8
template <int v>
void foo()
{
// not an lvalue, `v` is a template parameter of scalar type int
const int* a = &v; // ill-formed

v = 3; // ill-formed: lvalue required as left operand of assignment
}
  • 一个lambda表达式,例如[](int x){ return x * x; };(C++11起)
  • 一个require表达式,例如requires (T i) { typename T::type; };(C++20起)
  • 一个概念的特化,例如std::equality_comparable<int>。(C++20起)

特性:

  • 具有与右值相同的特性
  • 纯右值不能是多态的:他所表示的对象的动态类型始终是表达式的类型
  • 非类非数组的纯右值不能有cv限定符,除非它被物化以绑定到一个cv限定的引用类型上(C++17起)。(注意:函数调用或类型转换表达式可能产生具有cv限定的非类类型的纯右值,但cv限定符通常会被立即移除。)
  • 纯右值不能具有不完全类型(除了void类型,或用于decltype说明符中)
  • 纯右值不能具有抽象类类型或该类类型的数组类型
将亡值常见形式以及特性 点击查看更多
  • a.m,对象成员访问表达式。其中a是右值且m是对象类型的非静态数据成员。
  • p->m,内置的成员指针的表达式,其中m得是成员枚举器或非静态成员函数
  • a.*mp,通过成员指针访问对象成员的表达式,其中mp是成员函数的指针;
  • p->*mp,内置的指向对象成员的指针表达式,其中mp是成员函数的指针;
  • a, b,内置的逗号表达式,其中b是纯右值;
  • a ? b : c,针对特定bc的三元表达式
  • 函数调用或重载运算符表达式,其返回类型是对象的右值引用,例如std::move(x);(C++11起)
  • a[n],内置的下标表达式,其中一个操作数是数组右值;(C++11起)
  • 一个转换为对象类型的右值引用的类型转换表达式,例如static_cast<char&&>(x);(C++11起)
  • 任何在临时物化(temporary materialization)之后指代该临时对象的表达式;(C++17起)
  • 可移动表达式。(C++23起)

特性:

  • 具有与右值相同的特性
  • 具有与泛左值相同的特性

将亡值同时具备泛左值和右值的特性,特别的,①就像所有右值一样,将亡值可以绑定到右值引用;②并且像所有泛左值一样,将亡值可以是多态的,并且非类类型的将亡值可以带有cv限定符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <type_traits>

template <class T> struct is_prvalue : std::true_type {};
template <class T> struct is_prvalue<T&> : std::false_type {};
template <class T> struct is_prvalue<T&&> : std::false_type {};

template <class T> struct is_lvalue : std::false_type {};
template <class T> struct is_lvalue<T&> : std::true_type {};
template <class T> struct is_lvalue<T&&> : std::false_type {};

template <class T> struct is_xvalue : std::false_type {};
template <class T> struct is_xvalue<T&> : std::false_type {};
template <class T> struct is_xvalue<T&&> : std::true_type {};

// Example from C++23 standard: 7.2.1 Value category [basic.lval]
struct A
{
int m;
};

A&& operator+(A, A);
A&& f();

int main()
{
A a;
A&& ar = static_cast<A&&>(a);

// 返回右值引用的函数调用是将亡值
static_assert(is_xvalue<decltype( (f()) )>::value);

// 对象是将亡值,m是非静态对象类型数据成员,表达式是将亡值
static_assert(is_xvalue<decltype( (f().m) )>::value);

// 转换为右值引用的转换表达式是将亡值
static_assert(is_xvalue<decltype( (static_cast<A&&>(a)) )>::value);

// Operator expression, whose return type is rvalue reference to object
static_assert(is_xvalue<decltype( (a + a) )>::value);

// 具名的A&& ar是左值,可取地址
static_assert(is_lvalue<decltype( (ar) )>::value);
[[maybe_unused]] A* ap = &ar;
}
泛左值常见形式以及特性 点击查看更多

一个泛左值表达式是左值或将亡值。

特性:

  • 泛左值可通过左值到右值、数组到指针或函数到指针的隐式转换,隐式转换为纯右值;
  • 泛左值可以是多态的:其所标识的对象的动态类型未必与表达式的静态类型相同;
  • 在表达式允许的情况下,泛左值可以具有不完整类型。
右值常见形式以及特性 点击查看更多

一个右值表达式是纯右值或将亡值。

特性:

  • 无法通过内置取址运算符获取右值的地址,例如:&int()&i++[3]&42&std::move(x)均为非法操作;
  • 右值不能作为内置赋值运算符或复合赋值运算符的左操作数;
  • 右值可用于初始化常量左值引用,此时该右值所标识的临时对象的生命周期将被延长至该引用的作用域结束;
  • 右值可用于初始化右值引用,此时该右值所标识的临时对象的生命周期将被延长至该引用的作用域结束;(C++11起)
  • 当右值用作函数实参,且存在两个重载函数可供选择时(一个接受右值引用参数,另一个接受常量左值引用参数),右值将绑定到右值引用的重载版本(因此,如果同时存在拷贝构造函数和移动构造函数,右值实参将调用移动构造函数;对于拷贝赋值运算符和移动赋值运算符亦然)。(C++11起)
1
表达式根据图1中的分类体系进行分类 ![](/hiddens/cpp-working-draft-learning/7-2-expr_prop/7-2-1-Expression_category_taxonomy.png)
  • (1.1) 泛左值(glvalue,generalized lvalue)一种表达式,其求值结果确定一个对象、位域或函数的身份。
  • (1.2) 纯右值(prvalue,pure rvalue)一种表达式,其求值结果用来:初始化一个对象或位域;或计算一个运算符的操作数(具体取决于其出现的上下文);或是一个类型为cv void的表达式
  • (1.3) 将亡值(xvalue,expiring value)是一种泛左值,它表示一个资源可以被重用的对象或位域(通常因为它即将结束生命周期)。
  • (1.4) 左值(lvalue)是指不是将亡值的泛左值。
  • (1.5) 右值(rvalue)是纯右值和将亡值的统称。
Paw5zx个人理解:点击查看更多
  • 泛左值:代表某个实体本身,它有具体身份,指向或引用一个具体的存储位置,可取地址。
  • 纯右值:求值结果要么是初始化某对象、要么用于计算一个运算结果,不具有独立身份。常见于字面值、返回值、临时对象。
  • 将亡值:资源可被接管的泛左值,常由std::move或函数返回右值引用产生
  • 左值:仍然具有身份、但未处于可被“接管资源”状态的表达式,例如变量名、解引用表达式等。
  • 右值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 泛左值
int a = 10;
int& ref = a; // ref是泛左值(同时也是左值)


a; // 表达式a是泛左值

// 纯右值
int x = 5 + 10; // 表达式 (5 + 10) 是纯右值
int y = func(); // 若func()返回非引用类型,则func()是纯右值

// 将亡值

struct X
{
X() {}
~X() { std::cout << "~X\n"; }
};

X makeX() { return X(); }

int main()
{
X&& r = makeX(); // makeX()是纯右值

X y = std::move(r); // std::move(r)是将亡值
}

// 左值
int n = 42;
n = 43; // n是左值

// 右值


2
每个表达式都唯一地归属于此分类体系中的三种基本类别之一:左值,将亡值,纯右值。

表达式所具有的这一属性称为它的值类别。

【注释:在[expr.compound]一节中对每个内建运算符的讨论都会指出:该运算符生成的结果值所属的值类别,以及它所期望的操作数的值类别。例如,内建的赋值运算符要求其左操作数是左值,右操作数是纯右值,结果是左值。用户自定义的运算符本质上是函数,它所期望和产生的值类别,则由其参数类型和返回类型决定。——注释结束】

3

【注释:从历史上看左值与右值之所以得名,是因为它们可以出现在赋值表达式的左侧与右侧(尽管如今这以不再普遍成立)。
泛左值(“generalized” lvalues),纯右值(“pure” rvalues),将亡值(“eXpiring” lvalues)。尽管有这些名称,但这些术语实际上分类的是表达式,而非值。——注释结束】

4

【注释:一个表达式属于将亡值,当且仅当它符合以下任意一种情况:

  • (4.1) 调用一个返回类型为指向对象类型的右值引用的函数(无论显式还是隐式)([expr.call]),其结果是将亡值。
  • (4.2) 执行一次转换为指向对象类型的右值引用的类型转换表达式([expr.type.conv][expr.dynamic.cast][expr.static.cast][expr.reinterpret.cast][expr.const.cast][expr.cast])时,其结果是将亡值。
  • (4.3) a subscripting operation with an xvalue array operand([expr.sub]
  • (4.4) 一个非静态,非引用类型的类数据成员的访问表达式(obj.member形式)是一个将亡值([expr.ref])。
  • (4.5) 第一个操作数是一个将亡值,第二个操作数是指向数据成员的指针([expr.mptr.oper])的成员指针运算表达式是将亡值。

一般而言,这条规则的效果可以总结为:

  • 具名右值引用被视为左值;
  • 对象的无名右值引用(是一个表达式)被视为将亡值;
  • 函数的右值引用,不论是否具名,一律被视为左值。

——注释结束】

【示例:

1
2
3
4
5
6
7
8
struct A {
int m;
};
A&& operator+(A, A);
A&& f();

A a;
A&& ar = static_cast<A&&>(a);

表达式f()f().mstatic_cast<a&&>(a)a + a是将亡值。表达式ar是左值。——示例结束】

暂停