Namespaces
Variants

Value categories

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Value categories
Order of evaluation
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

每个 C++ 表达式 (包含操作数的运算符、字面量、变量名等)都具有两个独立属性: 类型 值类别 。每个表达式都具有某种非引用类型,且每个表达式必定属于三大基本值类别之一: prvalue xvalue lvalue

  • glvalue (“广义”左值)是指其求值结果确定对象或函数身份的表达式;
  • prvalue (“纯”右值)是指其求值
  • 计算内置运算符的操作数值(此类纯右值没有 结果对象 ),或
  • 初始化对象(此类纯右值被称为具有 结果对象 )。
结果对象可以是变量、由 new表达式 创建的对象、通过 临时物化 创建的临时对象或其成员。注意非 void 类型的 被丢弃值表达式 具有结果对象(即物化的临时对象)。此外,每个类和数组纯右值都具有结果对象,除非它是 decltype 的操作数;
  • 一个 xvalue (“即将失效”的值)是表示其资源可被重用的对象的泛左值;
  • 一个 lvalue 是不属于 xvalue 的泛左值;
扩展内容

之所以在历史上被称为左值,是因为它们可以出现在赋值表达式的左侧。但通常情况并非总是如此:

void foo();
void baz()
{
    int a; // 表达式 `a` 是左值
    a = 4; // 正确,可以出现在赋值表达式的左侧
    int &b{a}; // 表达式 `b` 是左值
    b = 5; // 正确,可以出现在赋值表达式的左侧
    const int &c{a}; // 表达式 `c` 是左值
    c = 6;           // 错误,对只读引用进行赋值
    // 表达式 `foo` 是左值
    // 可通过内置取址运算符获取其地址
    void (*p)() = &foo;
    foo = baz; // 错误,对函数进行赋值
}
  • 一个 右值 是纯右值或亡值;
扩展内容

之所以在历史上被称为右值,是因为它们可以出现在赋值表达式的右侧。但通常情况并非总是如此:

#include <iostream>
struct S
{
    S() : m{42} {}
    S(int a) : m{a} {}
    int m;
};
int main()
{
    S s;
    // 表达式 `S{}` 是纯右值
    // 可以出现在赋值表达式的右侧
    s = S{};
    std::cout << s.m << '\n';
    // 表达式 `S{}` 是纯右值
    // 同样可以在左侧使用
    std::cout << (S{} = S{7}).m << '\n';
}

输出:

42
7

注意:此分类体系在过去的C++标准修订中经历了重大变更,详见下文 历史沿革

扩展内容

尽管名称如此,这些术语分类的是表达式,而非值。

#include <type_traits>
#include <utility>
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 {};
int main()
{
    int a{42};
    int& b{a};
    int&& r{std::move(a)};
    // 表达式 `42` 是纯右值
    static_assert(is_prvalue<decltype((42))>::value);
    // 表达式 `a` 是左值
    static_assert(is_lvalue<decltype((a))>::value);
    // 表达式 `b` 是左值
    static_assert(is_lvalue<decltype((b))>::value);
    // 表达式 `std::move(a)` 是亡值
    static_assert(is_xvalue<decltype((std::move(a)))>::value);
    // 变量 `r` 的类型是右值引用
    static_assert(std::is_rvalue_reference<decltype(r)>::value);
    // 变量 `b` 的类型是左值引用
    static_assert(std::is_lvalue_reference<decltype(b)>::value);
    // 表达式 `r` 是左值
    static_assert(is_lvalue<decltype((r))>::value);
}

目录

主要类别

左值

下列表达式为 左值表达式

扩展内容
void foo() {}
void baz()
{
    // `foo` 是左值
    // 可通过内置取址运算符获取其地址
    void (*p)() = &foo;
}
struct foo {};
template <foo a>
void baz()
{
    const foo* obj = &a;  // `a` 是左值,模板参数对象
}
扩展内容
int& a_ref()
{
    static int a{3};
    return a;
}
void foo()
{
    a_ref() = 5;  // `a_ref()` 是左值,返回类型为左值引用的函数调用
}
  • a = b a + = b a % = b ,以及所有其他内置的 赋值和复合赋值 表达式;
  • ++ a -- a ,即内置的 前置自增与前置自减 表达式;
  • * p ,即内置的 间接寻址 表达式;
  • a [ n ] p [ n ] ,即内置的 下标 表达式 ,其中在 a [ n ] 中有一个操作数是数组左值 (自 C++11 起)
  • a. m ,即 对象成员访问 表达式,除非 m 是成员枚举项或非静态成员函数,或者当 a 为右值且 m 为对象类型的非静态数据成员时;
扩展内容
struct foo
{
    enum bar
    {
        m // member enumerator
    };
};
void baz()
{
    foo a;
    a.m = 42; // ill-formed, lvalue required as left operand of assignment
}
struct foo
{
    void m() {} // non-static member function
};
void baz()
{
    foo a;
    // `a.m` is a prvalue, hence the address cannot be taken by built-in
    // address-of operator
    void (foo::*p1)() = &a.m; // ill-formed
    void (foo::*p2)() = &foo::m; // OK: pointer to member function
}
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 ,即内置的 指针成员访问 表达式,但排除 m 为成员枚举项或非静态成员函数的情况;
  • a. * mp ,即 对象成员指针访问 表达式,其中 a 为左值且 mp 为指向数据成员的指针;
  • p - > * mp ,即内置的 指针成员指针访问 表达式,其中 mp 为指向数据成员的指针;
  • a, b ,即内置的 逗号 表达式,其中 b 为左值;
  • a ? b : c ,即针对特定 b c 三元条件 表达式(例如当两者为同类型的左值时,具体细节请参阅 定义 );
  • 字符串字面量 ,例如 "Hello, world!"
  • 转换为左值引用类型的强制转换表达式,例如 static_cast < int & > ( x ) static_cast < void ( & ) ( int ) > ( x )
  • 左值引用类型的常量 模板参数
template <int& v>
void set()
{
    v = 5; // 模板参数是左值
}
int a{3}; // 静态变量,编译期已知固定地址
void foo()
{
    set<a>();
}
  • 函数调用或重载运算符表达式,其返回类型为函数右值引用;
  • 转换为函数右值引用类型的强制转换表达式,例如 static_cast < void ( && ) ( int ) > ( x )
(since C++11)

属性:

  • 泛左值 (见下文)相同。
  • 左值的地址可通过内置取址运算符获取: & ++ i [1] & std:: hex 均为合法表达式。
  • 可修改的左值可作为内置赋值运算符及复合赋值运算符的左操作数。
  • 左值可用于 初始化左值引用 ;这将使新名称与表达式所标识的对象建立关联。

prvalue

下列表达式属于 纯右值表达式

template <int v>
void foo()
{
    // 不是左值,`v` 是标量类型 int 的模板参数
    const int* a = &v; // 非法的
    v = 3; // 非法的:赋值运算的左操作数需要左值
}
(C++11 起)
(C++20 起)

属性:

xvalue

下列表达式属于 xvalue表达式

  • a. m ,即 对象成员 表达式,其中 a 为右值且 m 为对象类型的非静态数据成员;
  • a. * mp ,即 对象成员指针 表达式,其中 a 为右值且 mp 为指向数据成员的指针;
  • a, b ,即内置 逗号 表达式,其中 b 为亡值;
  • a ? b : c ,即特定 b c 条件下的 三元条件 表达式(详见 定义 );
  • 函数调用或重载运算符表达式,其返回类型为对象的右值引用,例如 std :: move ( x )
  • a [ n ] ,内置的 下标 表达式,且其中一个操作数为数组右值;
  • 转换为对象类型右值引用的强制转换表达式,例如 static_cast < char && > ( x )
(C++11 起)
(C++17 起)
(C++23 起)

属性:

  • 与右值(见下文)相同。
  • 与泛左值(见下文)相同。

具体而言,与所有右值一样,xvalue 可绑定到右值引用;与所有泛左值一样,xvalue 可以是 多态 的,且非类类型的 xvalue 可以被 cv 限定

扩展内容
#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 {};
// 示例来自 C++23 标准:7.2.1 值类别 [basic.lval]
struct A
{
    int m;
};
A&& operator+(A, A);
A&& f();
int main()
{
    A a;
    A&& ar = static_cast<A&&>(a);
    // 返回类型为右值引用的函数调用是 xvalue
    static_assert(is_xvalue<decltype( (f()) )>::value);
    // 对象表达式的成员,对象为 xvalue,`m` 是非常态数据成员
    static_assert(is_xvalue<decltype( (f().m) )>::value);
    // 转换为右值引用的强制转换表达式
    static_assert(is_xvalue<decltype( (static_cast<A&&>(a)) )>::value);
    // 运算符表达式,其返回类型为对象的右值引用
    static_assert(is_xvalue<decltype( (a + a) )>::value);
    // 表达式 `ar` 是 lvalue,`&ar` 是有效的
    static_assert(is_lvalue<decltype( (ar) )>::value);
    [[maybe_unused]] A* ap = &ar;
}

混合类别

glvalue

一个 glvalue 表达式 要么是左值,要么是亡值。

属性:

  • glvalue 可通过左值到右值、数组到指针或函数到指针的 隐式转换 隐式转换为 prvalue。
  • glvalue 可以是 多态 的:其标识对象的 动态类型 不一定与表达式的静态类型相同。
  • 在表达式允许的情况下,glvalue 可以具有 不完整类型

右值

一个 右值表达式 要么是纯右值,要么是亡值。

属性:

  • 右值的地址无法通过内置取址运算符获取: & int ( ) & i ++ [3] & 42 以及 & std :: move ( x ) 均无效。
  • 右值不能作为内置赋值或复合赋值运算符的左操作数。
  • 右值可用于 初始化常量左值引用 ,此时该右值所标识的临时对象的生命周期将 被延长 至引用作用域结束。
  • 右值可用于 初始化右值引用 ,此时该右值所标识的临时对象的生存期将 被延长 至该引用作用域结束。
  • 当作为函数实参使用且存在 两个重载版本 时——一个接受右值引用参数,另一个接受常量左值引用参数——右值将绑定至右值引用重载版本(因此,若同时存在拷贝构造函数和移动构造函数,右值实参会调用 移动构造函数 ,拷贝赋值运算符与移动赋值运算符同理)。
(since C++11)

特殊类别

待决成员函数调用

表达式 a. mf p - > mf (其中 mf 非静态成员函数 ),以及表达式 a. * pmf p - > * pmf (其中 pmf 成员函数指针 ),均被归类为纯右值表达式。但这些表达式不能用于初始化引用、作为函数参数,或用于除函数调用运算符左操作数之外的任何用途,例如: ( p - > * pmf ) ( args )

Void 表达式

返回 void 的函数调用表达式、转换为 void 的强制转换表达式以及 throw 表达式 均被归类为纯右值表达式,但它们不能用于初始化引用或作为函数参数。这些表达式可用于弃值上下文(例如单独成行、作为逗号运算符的左操作数等),也可用于返回 void 的函数中的 return 语句。此外,throw 表达式可作为 条件运算符 ?: 的第二和第三操作数使用。

Void表达式没有 结果对象

(since C++17)

位域

指定位域(例如 a. m ,其中 a 是类型为 struct A { int m : 3 ; } 的左值)的表达式是泛左值表达式:它可以作为赋值运算符的左操作数,但不能获取其地址,且非 const 左值引用无法绑定到它。const 左值引用或右值引用可以从位域泛左值初始化,但会创建位域的临时副本:引用不会直接绑定到位域本身。

可移动表达式

尽管由任何变量名组成的表达式都是左值表达式,但当其作为以下操作数出现时,此类表达式可能具备可移动性:

若表达式具备可移动性,在 重载决议 时它会被视作 右值或左值 (C++23 前) 右值 (C++23 起) (因此可能选择 移动构造函数 )。详见 从局部变量和参数的自动移动

(C++11 起)

历史

CPL

编程语言 CPL 首次引入了表达式的值类别:所有CPL表达式都可以在"右值模式"下求值,但只有特定类型的表达式在"左值模式"下才有意义。在右值模式下求值时,表达式被视为计算值的规则(即右值,或 rvalue )。在左值模式下求值时,表达式实际给出一个地址(即左值,或 lvalue )。这里的"左"和"右"分别代表"赋值语句左侧"和"赋值语句右侧"。

C

C语言遵循了类似的分类体系,但赋值操作的作用已不再显著:C语言表达式被划分为"左值表达式"和其他类型(函数与非对象值),其中"左值"指代能够标识对象的表达式,即"定位符值" [4]

C++98

2011年前的C++沿用了C语言的模型,但恢复了"右值"这一名称用于指代非左值表达式,将函数设为左值,并新增了规则:引用可以绑定到左值,但只有常量引用才能绑定到右值。若干在C语言中属于非左值的表达式在C++中成为了左值表达式。

C++11

随着 C++11 中移动语义的引入,值类别被重新定义以表征表达式的两个独立属性 [5]

  • 具有标识 :可以确定表达式是否与另一个表达式引用同一实体,例如通过比较对象的地址或它们所标识的函数(直接或间接获取);
  • 可被移动 移动构造函数 移动赋值运算符 ,或其他实现移动语义的函数重载可以绑定到该表达式。

在 C++11 中,表达式满足以下条件:

  • 具有标识且不可被移动的表达式称为 左值 表达式;
  • 具有标识且可被移动的表达式称为 亡值 表达式;
  • 不具有标识且可被移动的表达式称为 纯右值 表达式;
  • 不具有标识且不可被移动的表达式未被使用 [6]

具有标识符的表达式被称为“泛左值表达式”(glvalue 是“generalized lvalue”的缩写)。左值和亡值都属于泛左值表达式。

能够被移动的表达式称为"右值表达式"。prvalue 和 xvalue 都属于右值表达式。

C++17

在 C++17 中, 拷贝消除 在某些场景下变为强制要求,这需要将纯右值表达式与其初始化的临时对象分离开来,从而形成了我们当前的体系。需要注意的是,与 C++11 的方案不同,纯右值不再被从中移出。

脚注

  1. 假设 i 具有内置类型,或前置递增运算符被 重载 为返回左值引用。
  2. 2.0 2.1 2.2 2.3 特殊右值类别,参见 待决成员函数调用
  3. 假设 i 具有内置类型,或后置递增运算符未被 重载 为返回左值引用。
  4. "C语言社区内部关于左值含义存在分歧,一派认为左值可以是任何类型的对象定位器,另一派则认为左值在赋值运算符左侧才具有意义。C89委员会采纳了将左值定义为对象定位器的方案。" -- ANSI C原理说明,6.3.2.1/10。
  5. 《新值分类术语》 ,Bjarne Stroustrup,2010年。
  6. 常量纯右值(仅允许类类型)和常量亡值不会绑定到 T&& 重载,但会绑定到 const T && 重载(这些重载也被标准归类为"移动构造函数"和"移动赋值运算符"),从而满足本分类中"可被移动"的定义。然而此类重载无法修改其参数且在实践中不被使用;当缺少这些重载时,常量纯右值和常量亡值会绑定到 const T & 重载。

参考文献

  • C++23 标准 (ISO/IEC 14882:2024):
  • 7.2.1 值类别 [basic.lval]
  • C++20 标准 (ISO/IEC 14882:2020):
  • 7.2.1 值类别 [basic.lval]
  • C++17 标准 (ISO/IEC 14882:2017):
  • 6.10 左值与右值 [basic.lval]
  • C++14 标准 (ISO/IEC 14882:2014):
  • 3.10 左值与右值 [basic.lval]
  • C++11 标准 (ISO/IEC 14882:2011):
  • 3.10 左值与右值 [basic.lval]
  • C++98 标准 (ISO/IEC 14882:1998):
  • 3.10 左值与右值 [basic.lval]

缺陷报告

下列行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 应用于 发布时行为 正确行为
CWG 616 C++11 对右值的成员访问及通过成员指针的访问会产生纯右值 重新分类为亡值
CWG 1059 C++11 数组纯右值不能具有cv限定符 允许限定
CWG 1213 C++11 对数组右值进行下标操作会产生左值 重新分类为亡值

参阅

C 文档 关于 值类别

外部链接

1. C++ 值类别与 decltype 原理解析 — David Mazières, 2021
2. 表达式值类别的实验性判定方法 — StackOverflow