Monday, November 1, 2021

[C++] Value category

lvalue : left-hand value

一个占据内存中某个可识别的位置(也就是一个地址)的对象。
Address of an lvalue may be taken

prvalue : pure right-hand value

纯右值是表达式产生的中间值(临时值),不能取地址
右值一定会在表达式结束后被销毁,比如return x(x被copy以后会被销毁), 1+2(3这个中间值会被销毁)

xvalue : eXpiring value

消亡值就是通过右值引用产生的值

 lvalue reference:

(可以理解为内存地址, 指针值)

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

const左值引用是可以指向右值的:

const int &ref_a = 5;  // 编译通过

rvalue reference:

(&ref_a_right 也可理解为地址,指针值,但是可以是临时的)

https://stackoverflow.com/questions/26372292/what-does-it-mean-to-take-the-address-of-an-rvalue-reference

ex: 
void foo( int&& x ) {

    std::cout << &x;
}
int y = 5; foo(static_cast<int&&>(y)); does not create a temporary.
A temporary will be created if for example you call foo(1);


右值引用专门为右值而生,可以指向右值,不能指向左值

int &&ref_a_right = 5; // ok
 
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
 
ref_a_right = 6; // 右值引用的用途:可以修改右值

而此时被声明出来的 ref_a_right 本身是一个lvalue : 可获取地址

作为函数返回值的 && 是右值,直接声明出来的 && 是左值

int &&ref = std::move(a);

std::move会返回一个右值引用int &&,它是左值还是右值呢? 从表达式来看,右值引用ref指向的必须是右值,所以move返回的int &&是个右值。

右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值

 被声明出来的左、右值引用都是左值

gvalue = lvalue or xvalue

rvalue = prvalue or xvalue


lvalue 与 rvalue 之间的转换

from lvalue to rvalue : 
计算符隐式转换
int a = 1;     // a 是左值
int b = 2; // b 是左值
int c = a + b; // + 需要右值,所以 a 和 b 被转换成右值
// + 返回右值

int arr[] = {1, 2};
int* p = &arr[0]; //arr[0] 是 左值
*(p + 1) = 10; // 正确: p + 1 是右值,但 *(p + 1) 是左值
int var = 10;
int* bad_addr = &(var + 1); // 错误: 一元 '&' 操作符需要左值参数
int* addr = &var; // 正确: var 是左值
&var = 40; // 错误: 赋值操作的左操作数需要是左值

显式 : std::move(T lvalue) { static_cast<T&&>(lvalue) }

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
 
cout << a; // 打印结果:5

std::move是一个非常有迷惑性的函数,不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)。 所以,单纯的std::move(xxx)不会有性能提升

右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:

int &&ref_a = 5;
ref_a = 6; 
 
等同于以下代码:
 
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;


  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

std::move

 indicate that an object t may be "moved from"
std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type. : static_cast<T&&>(lvalue) 
是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了.
     Array a;
    // 左值a,用std::move转化为右值
    Array b(std::move(a));


// Simple move constructor
A(A&& arg) : member(std::move(arg.member)) // the expression "arg.member" is lvalue
{} 
// Simple move assignment operator
A& operator=(A&& other) {
     member = std::move(other.member);
     return *this;
}


可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。

还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):

std::unique_ptr<A> ptr_a = std::make_unique<A>();

std::unique_ptr<A> ptr_b = std::move(ptr_a); // unique_ptr只有‘移动赋值重载函数‘,参数是&& ,只能接右值,因此必须用std::move转换类型

std::unique_ptr<A> ptr_b = ptr_a; // 编译不通过

std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。

std::move is just indicate that this object maybe move from, it doesn't do any "move". The key is that the type should have a  move constructor or move assignment operator

To be movable.


move an int == a copy: 

    int a = 3;

    int aa = std::move(a);

    int &&rRef = std::move(a);

    rRef = 4;//rvalue reference can change the value, like lvalue reference


    std::cout<< a << std::endl;//4

    std::cout<< aa << std::endl;//3


move string: --> string has move assignement and move constructor : 

   std::string str = "Salut";

    std::vector<std::string>  v;

    v.push_back(std::move(str));


    std::cout << str <<endl; //"" empty string : moved, 

    std::cout << v[0] <<endl; //"Salut"

std::forward<T>

Forwards lvalues as either lvalues or as rvalues, depending on T

void B(int&& ref_r) {
    ref_r = 1;
}
 
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
    B(ref_r);  // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    B(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
 
int main() {
    int a = 5;
    A(std::move(a));
}

ex2

void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
     
    change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
    change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
    // 可见,forward可以把值转换为左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}



Ref :  

https://en.cppreference.com/w/cpp/language/value_category

https://www.cnblogs.com/philip-tell-truth/p/6370019.html

http://c.biancheng.net/view/1510.html

https://blog.csdn.net/fefe82/article/details/50181873

https://nettee.github.io/posts/2018/Understanding-lvalues-and-rvalues-in-C-and-C/

https://medium.com/@winwardo/c-moves-for-people-who-dont-know-or-care-what-rvalues-are-%EF%B8%8F-56ee122dda7


No comments:

Post a Comment