C++编程:理解左值(lvalue)和右值(rvalue)

C++ 值的分类(Value Categories)

目录

[1 概述](#1 概述)

[2 主要分类](#2 主要分类)

[1.1 左值(lvalue)](#1.1 左值(lvalue))

[1.1.1 左值详情](#1.1.1 左值详情)

[1.1.2 左值属性](#1.1.2 左值属性)

[1.2 纯右值(prvalue)](#1.2 纯右值(prvalue))

[1.2.1 纯右值详情](#1.2.1 纯右值详情)

[1.2.2 纯右值属性](#1.2.2 纯右值属性)

[1.3 将逝值(xvalue)](#1.3 将逝值(xvalue))

[1.3.1 将逝值详情](#1.3.1 将逝值详情)

[1.3.2 将逝值属性](#1.3.2 将逝值属性)

[3 混合分类](#3 混合分类)

[3.1 泛型左值(glvalue)](#3.1 泛型左值(glvalue))

[3.1.1 泛型左值详情](#3.1.1 泛型左值详情)

[3.1.2 泛型左值属性](#3.1.2 泛型左值属性)

[3.2 右值(rvalue)](#3.2 右值(rvalue))

[3.2.1 右值详情](#3.2.1 右值详情)

[3.2.2 右值属性](#3.2.2 右值属性)


1 概述

每一个C++ 表示达(带操作数的操作符、文字量、变量名、等等)都由两个独立属性刻画:类型 (type)及类型值所属分类 (type value category),值类别是编译器在表达式求值期间创建、复制和移动临时对象时必须遵循的规则的基础。每一个表达式都有一些非引用表达式,且每一个表达式都恰好归属于三种主要的值分类之一:纯右值(prvalue ),将逝值(xvalue )和左值(lvalue)。这些分类的关系如下图:

主次分类为:

(1) 泛型左值 (glvalue------"generalized" lvalue): 是一个表达式,其求值( evaluation ) 决定了对象,位域,或函数的身份。

(2) 纯右值 (prvalue------"pure"rvalue):是一个表达式,其求值

a. 计算内置运算符的操作数的值(此类 prvalue 没有结果对象),或

b. 初始化一个对象(这种prvalue被认为有一个结果对象)。

结果对象可以是变量、由 new 表达式创建的对象、由临时实现创建的临时对象或其成员。请注意,非 void 废弃值表达式具有结果对象(实现的临时对象)。此外,每个类和数组 prvalue 都有一个结果对象,除非它是 decltype 的操作数;

(3) 将逝值 (xvalue------"eXpiring"value,即将消亡(或正在消亡)的值): 是一个泛型左值,表示一个对象的资源可以重用,将逝值表达式具有一个地址,该地址不供程序访问,但可用于初始化右值引用,从而提供对表达式的访问。示例包括返回右值引用的函数调用,以及数组下标、成员和指向成员的指针表达式,其中数组或对象是右值引用。;

(4) 左值 (lvalue):左值是非将逝值的泛型左值其具有一个程序可访问的地址 (注:左值就是可通过直接取其内存地址进行操作的对象,而右值没有可供程序访问的地址 )。从历史上看,之所以这样称呼,是因为左值可以出现在赋值表达式的左侧一般来说,情况并非总是如此

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; // 错误, 不能将函数赋值给函数地址

}

(5) 右值(rvalue):是纯粹的右值或将逝值。从历史上看,之所以这样称呼,是因为右值可以出现在赋值表达式的右侧。一般来说,情况也并非总是如此:

复制代码
#include <iostream>
复制代码
 
复制代码
struct S
复制代码
{
复制代码
    S() : m{42} {}
复制代码
    S(int a) : m{a} {}
复制代码
    int m;
复制代码
};
复制代码
 
复制代码
int main()
复制代码
{
复制代码
    S s;
复制代码
 
复制代码
    // 表达式 `S{}` 是一个纯粹右值
复制代码
    // 可以出现在赋值表达式的右侧
复制代码
    s = S{}; // 0初始化
复制代码
 
复制代码
    http://en.cppreference.com/w/cpp/io/cout << s.m << '\\n';
复制代码
 
复制代码
    // 表达式 `S{}` 是一个纯粹的右值
复制代码
    // 也可以用在表达式的左边
复制代码
    http://en.cppreference.com/w/cpp/io/cout << (S{} = S{7}).m << '\\n';
复制代码
}

输出:

42

7

注意:此分类法在过去的 C++ 标准修订中经历了重大变化,详情请参阅下面的历史记录。

尽管这些术语有这样的名字,但它们是对表达式进行分类,而不是对值进行分类。

复制代码
#include <type_traits>
复制代码
#include <utility>
复制代码
 
复制代码
template <class T> struct is_prvalue : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
template <class T> struct is_prvalue<T&> : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
template <class T> struct is_prvalue<T&&> : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
 
复制代码
template <class T> struct is_lvalue : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
template <class T> struct is_lvalue<T&> : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
template <class T> struct is_lvalue<T&&> : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
 
复制代码
template <class T> struct is_xvalue : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
template <class T> struct is_xvalue<T&> : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
template <class T> struct is_xvalue<T&&> : http://en.cppreference.com/w/cpp/types/integral_constant {};
复制代码
 
复制代码
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(http://en.cppreference.com/w/cpp/types/is_rvalue_reference<decltype(r)>::value);
复制代码
 
复制代码
    //变量类型`b` 是一个左值引用
复制代码
    static_assert(http://en.cppreference.com/w/cpp/types/is_lvalue_reference<decltype(b)>::value);
复制代码
 
复制代码
    //表达式`r`是左值
复制代码
    static_assert(is_lvalue<decltype((r))>::value);
复制代码
}

2 主要分类

1.1 左值(lvalue)

1.1.1 左值详情

下面的表达式是左值表达式:

(1) 变量、函数、模板参数对象(自 C++20 起)或数据成员的名称,无论类型如何,例如 std::cin 或 std::endl。即使变量的类型是右值引用,由其名称组成的表达式也是左值表达式(但请参阅可移动表达式)。

复制代码
void foo() {}
复制代码
 
复制代码
void baz()
复制代码
{
复制代码
    // `foo` 是一个左值
复制代码
    // 可通过直接取其内存地址进行访问
复制代码
    void (*p)() = &foo;
复制代码
}
复制代码
复制代码
struct foo {};
复制代码
 
复制代码
template <foo a>
复制代码
void baz()
复制代码
{
复制代码
    const foo* obj = &a;  // `a` 是一个左值, 模板参数对象
复制代码
}

(2) 函数调用或重载运算符表达式,其返回类型为左值引用,例如 std::getline(std::cin, str)、std::cout << 1、str1 = str2 或 ++it。

复制代码
int& a_ref()
复制代码
{
复制代码
    static int a{3};
复制代码
    return a;
复制代码
}
复制代码
 
复制代码
void foo()
复制代码
{
复制代码
a_ref() = 5;  // `a_ref()` 是一个左值, 函数调用其返回类型是一个左值引用
复制代码
}

(3) a = b、a += b、a %= b 以及所有其他内置赋值和复合赋值是左值表达式。

(4) ++a 和 --a,内置的前增量和前减量表达式是左值表达式。

(5)*p,内置间接左值表达式。

**(6)**a[n] 和 p[n],内置下标表达式,其中 a[n] 中的一个操作数是数组左值(自 C++11 起)。

(7) a.m,对象表达式的成员,除非 m 是成员枚举器或非静态成员函数,或者 a 是右值且 m 是对象类型的非静态数据成员。

复制代码
struct foo
复制代码
{
复制代码
    enum bar
复制代码
    {
复制代码
        m //成员枚举器
复制代码
    };
复制代码
};
复制代码
 
复制代码
void baz()
复制代码
{
复制代码
    foo a;
复制代码
    a.m = 42; // 错, 需要左值作为赋值的左操作数
复制代码
}
复制代码
struct foo
复制代码
{
复制代码
    void m() {} // 非静态成员函数
复制代码
};
复制代码
 
复制代码
void baz()
复制代码
{
复制代码
    foo a;
复制代码
 
复制代码
    // `a.m` 是一个纯右值, 因此不能通过内置取地址运算符取得其地址
复制代码
    void (foo::*p1)() = &a.m; // 错
复制代码
 
复制代码
    void (foo::*p2)() = &foo::m; // OK: 成员函数指针
复制代码
}

**(8)**p->m,指针表达式的内置成员,除非 m 是成员枚举器或非静态成员函数。

**(9)**a.*mp,指向对象表达式的成员的指针,其中 a 是左值,mp 是指向数据成员的指针。

**(10)**p->*mp,指针表达式的内置指向成员的指针,其中mp是指向数据成员的指针。

**(11)**a,b,内置逗号表达式,其中b是左值。

**(12)**a ? b : c,对于特定的 b 和 c 的三元条件表达式(例如,当两者都是同一类型的左值时,但请参阅定义以了解详细信息)。

**(13)**字符串文字量,例如 "Hello, world!";

**(14)**转换为左值引用类型的转换表达式,例如 static_cast<int&>(x) 或 static_cast<void(&)(int)>(x);

**(15)**左值引用类型的非类型模板参数;

复制代码
template <int& v>
复制代码
void set()
复制代码
{
复制代码
    v = 5; // 模板参数是左值
复制代码
}
复制代码
 
复制代码
int a{3}; //静态变量, 固定地址在编译时已知
复制代码
 
复制代码
void foo()
复制代码
{
复制代码
    set<a>();
复制代码
}

**(16)**函数调用或重载运算符表达式,其返回类型是函数的右值引用(C++11及以上版本)。

**(17)**强制转换表达式为函数类型的右值引用,例如 static_cast<void(&&)(int)>(x) (C++11及以上版本)。

1.1.2 左值属性

(1) 与泛型左值相同(见下文)。

(2) 左值的地址可由内置地址运算符获取:&++i[1] 和 &std::endl 是有效表达式。

**(3)**可修改的左值可用作内置赋值和复合赋值运算符的左侧操作数。

**(4)**左值可用于初始化左值引用;这会将新名称与表达式标识的对象关联起来。

1.2 纯右值(prvalue)

1.2.1 纯右值详情

下面的表达式是纯右值表达式:

(1) 字面量(字符串字面量除外),例如 42、truenullptr

**(2)**函数调用或重载运算符表达式,其返回类型为非引用,例如 str.substr(1, 2)、str1 + str2 或 it++。

**(3)**a++ 和 a--,内置后增和后减表达式。

**(4)**a + b、a % b、a & b、a << b 和所有其他内置算术表达式。

**(5)**a && b、a || b、!a,内置逻辑表达式。

**(6)**a < b、a == b、a >= b 和所有其他内置比较表达式。

(7)&a,内置地址表达式。

**(8)**a.m,对象成员表达式,其中 m 是成员枚举器或非静态成员函数[2] 。

**(9)**p->m,指针表达式的内置成员,其中 m 是成员枚举器或非静态成员函数[2] 。

**(10)**a.*mp,指向对象成员的指针表达式,其中 mp 是指向成员函数的指针[2] 。

**(11)**p->*mp,指向指针表达式的内置成员的指针,其中 mp 是指向成员函数的指针[2] 。

**(12)**a, b,内置逗号表达式,其中 b 是纯右值。

**(12)**a ? b : c,针对特定 b 和 c 的三元条件表达式(详情请参阅定义)。

**(13)**转换为非引用类型的强制转换表达式,例如 static_cast<double>(x)、std::string{} 或 (int)42。

**(14)**this 指针。

**(15)**枚举器。

**(16)**标量类型的非类型模板参数。

复制代码
template <int v>
复制代码
void foo()
复制代码
{
复制代码
    // 非左值, `v` 是一个类型为int标量参数模板
复制代码
    const int* a = &v; // 错
复制代码
 
复制代码
    v = 3; // 错: 需要左值作为赋值的左操作数
复制代码
}

**(17)**lambda 表达式,例如 [](int x){ return x * x; }(C++11及以上版本)。

**(18)**需要表达式,例如需要 (T i) { typename T::type; }(C++11及以上版本)。

**(19)**概念的特化,例如 std::equality_comparable<int>(C++11及以上版本)。

1.2.2 纯右值属性

**(1)**与右值相同(见下文)。

**(2)**纯右值不能是多态的:它表示的对象的动态类型始终是表达式的类型。

**(3)**非类非数组右值不能是 cv修饰的的,除非它被具体化以绑定到对 cv 修饰类型的引用(C++17及以上版本)。(注意:函数调用或强制转换表达式可能导致非类 cv 修饰类型的 右值,但 cv 修饰符通常会立即被删除。)

(4) 右值不能具有不完整类型(void 类型除外,见下文,或在 decltype 说明符中使用时)。

**(5)**右值不能具有抽象类类型或其数组。

1.3 将逝值(xvalue)

1.3.1 将逝值详情

下面的表达式是将逝值表达式:

**(1)**a.m,对象成员表达式,其中 a 是右值,m 是对象类型的非静态数据成员。

**(2)**a.*mp,对象成员指针表达式,其中 a 是右值,mp 是数据成员指针。

**(3)**a, b,内置逗号表达式,其中 b 是 将逝值。

**(4)**a ? b : c,针对特定 b 和 c 的三元条件表达式(详情请参阅定义)。

**(5)**函数调用或重载运算符表达式,其返回类型为对象的右值引用,例如 std::move(x)(C++11及以上版本)。

**(6)**a[n],内置下标表达式,其中一个操作数是数组右值(C++11及以上版本)。

**(7)**强制转换为对象类型的右值引用的表达式,例如 static_cast<char&&>(x) (C++11及以上版本)。

**(8)**任何指定临时对象的表达式,在临时实现之后(C++17及以上版本)。

**(9)**可移动的表达式。(C++23及以上版本)。

1.3.2 将逝值属性

(1) 同右值(下述)。

(2) 同泛左值(下述)。

具体来说,与所有右值一样,将逝值绑定到右值引用,并且与所有泛左值一样,将逝值可以是多态的,并且非类 将逝值可以是 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 {};

// 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);

// 运算符表达式,其返回类型是对象的右值引用

static_assert(is_xvalue<decltype( (a + a) )>::value);

// 表达式`ar`左值, `&ar` 有效

static_assert(is_lvalue<decltype( (ar) )>::value);

[[maybe_unused]] A* ap = &ar;

}

3 混合分类

3.1 泛型左值(glvalue)

3.1.1 泛型左值详情

泛型左值表达式要么是左值,要么是将逝值。

3.1.2 泛型左值属性

**(1)**泛左值可以通过左值到右值、数组到指针或函数到指针的隐式转换隐式转换为纯右值。

**(2)**泛左值可以是多态的:它所标识的对象的动态类型不一定是表达式的静态类型。

**(3)**泛左值可以具有不完整类型,只要表达式允许。

3.2 右值(rvalue)

3.2.1 右值详情

右值表达式要么是纯右值,要么是将逝值。

3.2.2 右值属性

(1) 右值的地址不能由内置地址运算符获取:&int(),&i++[3],&42 和 &std::move(x) 等操作无效。

**(2)**右值不能用作内置赋值或复合赋值运算符的左侧操作数。

**(3)**右值可用于初始化 const 左值引用,在这种情况下,右值标识的临时对象的生存期将延长,直到引用的范围结束。

**(4)**右值可用于初始化右值引用,在这种情况下,右值标识的临时对象的生存期将延长,直到引用的范围结束(C++11 及以上版本)。

**(5)**当用作函数参数并且该函数有两个重载可用时,一个采用右值引用参数,另一个采用对 const 参数的左值引用,则右值绑定到右值引用重载(因此,如果复制和移动构造函数都可用,则右值参数会调用移动构造函数,复制和移动赋值运算符也是如此) (C++11 及以上版本)。

相关推荐
A懿轩A1 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
机器视觉知识推荐、就业指导1 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香1 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Ronin3052 小时前
11.vector的介绍及模拟实现
开发语言·c++
✿ ༺ ོIT技术༻2 小时前
C++11:新特性&右值引用&移动语义
linux·数据结构·c++
字节高级特工2 小时前
【C++】深入剖析默认成员函数3:拷贝构造函数
c语言·c++
唐诺8 小时前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨9 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客9 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin9 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin