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

C++ 值的分类(Value Categories)

目录

1  概述

2  主要分类

1.1  左值(lvalue)

1.1.1  左值详情

1.1.2  左值属性

1.2  纯右值(prvalue)

1.2.1  纯右值详情

1.2.2  纯右值属性

1.3  将逝值(xvalue)

1.3.1  将逝值详情

1.3.2  将逝值属性

3  混合分类

3.1  泛型左值(glvalue)

3.1.1  泛型左值详情

3.1.2  泛型左值属性

3.2  右值(rvalue)

3.2.1 右值详情

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初始化
 
    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);
}

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 及以上版本)。

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值