C++17中的VALUE CATEGORIES


一 定义

表达式用于表达一个value, value有两种属性, 一种是value type,一种是value categary。 value category 通过两种属性分为三类:

  1. has identity: identity 用来区分两个expression是否指向同一对象,比如比较它们的地址。(ps:我认为有identity不一定有地址, 因为它有可能在寄存器中)
  2. 可以通过移动语义移动: 移动构造, 移动赋值,用户定义带有移动语义的函数

lvalue expression

在evaluate时决定对象或函数的identity, 表明了接下来要对这个对象或者函数操作( has identity, 不可以移动)

xvalue expression

存储的value可以被复用, 即被移动, 移动后指向的对象除了析构不可以再被使用(has identity, 可以移动)

prvalue expression

没有identity, 不可以被移动。它有以下两种情况。
用于内置运算符计算操作数的值, (int a = 3 + b;)。
用于初始化一个对象,它本身并不是一个对象, 属于object representation(或者是 value representation?)(ps: 在我的理解里, 当prvalue没有指定要初始化的对象的时候, 它就会创建一个临时对象初始化,即使有要初始化的对象, 实现也有可能创建一个临时对象。 如 T obj = T(), 用于初始化obj。如 T && ref = T(), 初始化ref引用的prvalue temporary materialization的临时对象, 见下面的三 temporary)。
我甚至认为这
glvalue:指向一个对象 rvalue:现在好像概念上意义已经不大了, 只是作为xvalue和prvalue的统称。

二 转变

1) glvalue转换为prvalue

左值到右值的转换, 隐式转换中的标准转换序列中的一种。class type会调用拷贝构造的转换语义, 产生一个prvalue(不是一个对象,但是也必然产生一个对象)。可以看做读取对象的值的方式。

2) lvalue 转换成 xvalue

std::move 或者 static_cast<std::remove_reference_t&&>, 改变值的value category, 并不会产生新的对象,通过这种方式将lvalue用于移动语义。

3) prvalue初始化glvalue指向的对象 (prvalue semantic, guaranteed copy elsion)

prvalue 转换为 xvalue

隐式转换中的temporary materialization, 会创建一个xvalue指向的临时对象, prvalue用于初始化这个临时对象。
T &&ref = T();

prvalue转换为 lvalue

T a = T(),有prvalue semantic, 即 T a = T(T(T())) 也只初始化了a指向的对象, 并不会临时物化, 纯右值语义使得copy initialization也和direct initializaion一样, 不会调用不必要的copy/move。
注意拷贝初始化, 直接初始化和赋值语义的区别,以下代码:
T a = T(); //拷贝初始化
// T()是一个纯右值表达式
// 在C++11中, 它需要调用拷贝构造或移动构造(所以拷贝或移动构造必须eligible), 初始化拷贝构造或移动构造的引用类型的形参, 所以会临时物化。 编译器可能优化拷贝移动构造,gcc可以用-fno-elide-constructors来防止优化
// 在C++17中, 因为纯右值语义的存在,会尽量推迟临时物化, 所以T()纯右值直接用来初始化a,不会调用拷贝/移动构造

T a(3); // 直接初始化
// 并不是一个表达式,没有纯右值, 所以直接构造a。
// 构造函数有两个语义, 一个是构造, 一个是转换, 转换才是产生纯右值的语义, 所以当把构造函数声明成explicit的时候,拷贝初始化就不能使用

T a; a = T(); // 赋值
// 创建了两个对象, 一个对象为a, 因为第二个语句是赋值语义, 所以初始化了拷贝赋值或移动赋值的形参所代表的对象。所以该代码会调用移动赋值或拷贝赋值。

三 临时对象

两种情况会创建临时对象

1) temporary materializaion

物化出一个临时对象, 有以下几种情况

i) 给纯右值绑定引用

T&& ref = T(); const T &ref = T();

ii) 纯右值访问非静态成员对象, 或调用非静态成员函数

T().m_data; T().m_func();

iii) 数组纯右值转换为指针

不管是纯右值还是临时物化后的xvalue不是都不能取地址吗?怎么转换成pointer?没想到demo

iv) 数组纯右值下标访问元素

(int[3]){1, 2, 3}[2]; 物化了一个数组

v) discard value expression

full expression:T();
特殊案例,纯右值的上行转换 Base a = Derived();
个人理解为物化了一个Derived类型对象, 然后访问基类子对象(xvalue), 通过拷贝初始化初始化a。
这个基类子对象是在派生类临时对象物化出来时一起物化出来的。

vi) 使用 braced-enclosed initializer list 转换成的std::intializer_list

std::vector<int> v {1, 2, 3, 4, 5}; 物化一个有5个int类型元素的数组。
ps:两种情况可以使用std::initializer_list避免物化的数组被销毁。
i) 用于range-based for loop, C++23会延长range-initialier产生的临时对象的生命周期
for (auto &e : {1, 2, 3, 4})
{
std::cout << e << std::endl;
}

ii)将brace-init-list传递给函数
template
void func(std::initializer_list il)
{

}

func({1, 2, 3, 4}); // 语句1
因为语句1是一个full-expression, 所以物化的数组在语句1后才会被销毁, 在函数体内可以被安全的使用。

2)传递或返回trivally copyable type 的对象可能产生临时对象,由编译器决定

// gcc 13.2.1 std=c++17下测试
#include <iostream>
class A
{
private:
    int a;
public:
    A(int a = 0) :a(a) { identity();}
    // A(const A&ref) :a(ref.a) {}  
    // 如果有用户定义的拷贝构造, 那么这个class不是 trivially copyable type, 结果不同
    friend void func(A op1, A op2)
    {
        op2.identity();
        op1.identity();
    }
    void identity()
    {
        std::cout << "pointer:" << this << " value:" << a << std::endl;
    }
};
int main()
{
    std::cout <<  "A is trivally copyable type? " << std::is_trivially_copyable<A>::value << std::endl;
    func(A(3), A(4));
    return 0;
}

运行结果:
在这里插入图片描述

A(3)和 A(4) 按语义应该是直接初始化op1 和 op2的, 但是由于为了可能的性能提升, op1 和 op2用寄存器传递值而不是地址, 所以A(3) 和 A(4)分别产生了一个临时对象, 然后拷贝初始化op1, op2

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值