• C++语法——右值引用、移动构造和赋值、万能引用和转发、move和forward底层实现


    目录

    一.右值引用

    (一).何为右值

    (二).右值引用

    (三).右值和左值的互相传递

    ①左值->右值引用

    ②右值->左值引用

    (四).右值引用的自身属性

    二.移动构造和移动赋值

     (一).移动构造

     (二).移动赋值

    三.转发

    (一).万能引用

    (二).完美转发

    四.move和forward底层实现方式

    (一).move底层实现

    (二).forward底层实现


    一.右值引用

    (一).何为右值

    不能取地址的就是右值。例如:字面常量、临时变量。

    1. //1就是右值
    2. int i = 1;
    3. //max返回值是临时变量,也就是右值
    4. int n = max(1, 2);

    (二).右值引用

    左值引用是对左值的引用,顾名思义,右值引用就是对右值的引用。

    右值引用符号为&&,使用方式与左值引用相同,但符号和引用对象属性不同。

    左值引用右值引用
    符号&&&
    引用对象左值(可取地址的变量)右值(不可取地址)
    使用方式

    int j = 1;

    int& a = j;

    int&& a = 1;

    (三).右值和左值的互相传递

    左值和右值无法直接传递。

    1. int main()
    2. {
    3. int a = 1;
    4. int& b = a;//正确,左值引用
    5. int&& c = b;//错误。右值引用不能传左值
    6. int& d = 1;//错误。左值引用不能传右值
    7. return 0;
    8. }

    ①左值->右值引用

    该函数参数为左值,返回值为右值。

    作用是将左值参数强制转化成右值引用。 

    1. int a = 1;
    2. int&& c = move(a);//将a强转成右值

    ②右值->左值引用

    左值引用加上const修饰符即可。

    const int& d = 1;

    (四).右值引用的自身属性

    右值引用本身是左值属性。

    因此,如果给左值引用传递右值引用是可以的。

    1. int&& a = 1;//a为右值引用,但自身属性是左值
    2. int& b = a;//正确,给左值引用传递左值

    这该怎么理解呢?

    《C++ Primer》对此给出了相关解释,大意如下:

    左值是“持久”的,右值是“短暂”的。即左值只要不出作用域就可以一直存在,但右值只能在使用时的瞬间“存活”(参考函数返回值)。

    因此,当进行右值引用后,引用本身可以一直在作用域中存在,那么它就是左值。

    当然,可以使用另一种方式证明:取地址。

    左值可以取地址,右值不可以取地址。

    1. int main()
    2. {
    3. int a = 1;//a可以取地址,是左值
    4. int&& b = 3;
    5. cout << "a地址: " << &a << endl;
    6. cout << "b地址: " << &b << endl;
    7. return 0;
    8. }

     不妨总结一下:

    左值右值左值引用右值引用
    举例int a = 1;string str = "abc";int& b = a;int&& c = 1;
    属性左值右值左值左值
    取地址不能
    转化

    接收右值:

    直接传

    接收左值:

    接收右值:

    +const

    接收左值:

    move函数 

    二.移动构造和移动赋值

    C++11引入右值引用后,很大的作用便是移动构造与赋值。

    比如官方STL库中就提供了相关函数:

     (一).移动构造

    移动构造的目的在于减少因为参数是左值时引发的重复拷贝的问题。 

    以string为例进行说明:

    截取如下代码:

    1. class String {
    2. ...
    3. explicit String(const char* a = "")//默认构造
    4. {
    5. _size = strlen(a);
    6. _capacity = _size;
    7. _a = new char[_capacity + 1];
    8. strcpy(_a, a);
    9. cout << "构造函数\n";
    10. }
    11. String(const String& st)//拷贝构造
    12. :_a(nullptr)
    13. {
    14. String tmp(st.c_str());//调用构造函数
    15. swap(tmp);
    16. cout << "拷贝构造\n";
    17. }
    18. ...
    19. };
    20. String To_string(int value)//将int转为string
    21. {
    22. ...
    23. String str;
    24. ...
    25. return str;
    26. }
    1. int main()
    2. {
    3. String str = To_string(20);
    4. return 0;
    5. }

    当我们执行这个程序时,会调用2个默认构造和1个拷贝构造:

    分别是to_string内部生成str时调用默认构造、返回临时变量时调用string拷贝构造,但是string拷贝构造内部又会先调用默认构造。

    其实这还是优化后,如果没有编译器优化,main函数中str也会再调一次string拷贝构造。

     而这一切的“罪魁祸首”是什么呢?——to_string的返回值。

    是的,因为to_string内部会生成一个string对象,而该对象是局部变量,出了函数作用域就销毁,因此只能调用拷贝构造to_string内部的对象。

    这还只是string类型拷贝构造,如果是更加复杂的类型,拷贝构造往往会造成更多资源的占用。

    正因如此,移动构造派上了用场:

    1. String(String&& st)//移动构造函数,但是参数为右值
    2. :_a(nullptr)
    3. {
    4. swap(st);
    5. cout << "string移动构造\n";
    6. }

    移动构造的参数为右值,所以当to_string返回str时,会被移动构造接收。

    虽然str本身为左值属性,但是因为此时str是“将亡值”,即出了函数作用域就会被销毁,编译器会将这种“即将死亡”的值识别为右值。

    在移动构造内部,会将右值的数据与自身数据进行交换。因为右值作为“暂时存在的数据”,把数据交给目标对象,目标对象把“舍弃”的数据交给右值,正好可以“延续”目标数据且消除原本数据。

    这时,接收to_string返回值时只需要一个一个移动构造即可: 

     (二).移动赋值

    移动赋值的目的与移动构造类似,在于减少因为赋值造成重复拷贝的问题

    以string为例,其中赋值重载通过调用了拷贝构造函数实现。

    1. class String {
    2. ...
    3. String& operator=(const String& st)//赋值重载1
    4. {
    5. String tmp(st);//调用拷贝构造
    6. swap(tmp);
    7. cout << "string赋值\n";
    8. return *this;
    9. }
    10. String& operator=(const char* str)//赋值重载2
    11. {
    12. String tmp(str);
    13. swap(tmp);
    14. cout << "char*赋值\n";
    15. return *this;
    16. }
    17. ...
    18. };
    1. int main()
    2. {
    3. String str;
    4. cout << "--------------------------------\n";
    5. str = To_string(1);
    6. return 0;
    7. }

     当执行这个程序时,会有多个构造、拷贝构造被调用:

     而这其中,属于因为赋值重载而调用的就有三个。

     因为赋值重载的参数是左值引用,不能像右值引用那样交换数据,只能调用拷贝构造获取数据。

    由此,移动赋值应运而生:

    与移动构造相同,移动赋值也是直接与右值交换数据。 

    1. String& operator=(String&& st)
    2. {
    3. swap(st);
    4. cout << "string移动赋值\n";
    5. return *this;
    6. }

     此时,只需要将to_string的返回值作为右值传给移动赋值即可。

    三.转发

    (一).万能引用

    首先,万能引用只存在与模板编程中

    万能引用就是引用形参既可接收左值也可接收右值,其符号与右值引用相同,但必须是模板。

    即当模板的参数是右值引用的形式,如果实参是左值就是左值引用,右值就是右值引用。

     例如下列代码:

    1. void Print(int& a)
    2. {
    3. cout << "左值" << endl;
    4. }
    5. void Print(int&& a)
    6. {
    7. cout << "右值" << endl;
    8. }
    9. template<class T>
    10. void func(T&& t)//万能引用
    11. {
    12. Print(t);
    13. }
    14. int main()
    15. {
    16. int a = 0;
    17. func(a);//传左值
    18. func(1);//传右值
    19. return 0;
    20. }

    (二).完美转发

    上述代码有一个问题,尽管func(1)传入的是右值,但是因为右值引用本身是左值,当调用Print函数时,会调用左值版本,这不符合我们的预期,因为明明传入的是右值:

     这时,就需要使用完美转发forward,它会保持传入实参的属性不变:

    1. void func(T&& t)
    2. {
    3. Print(std::forward(t));
    4. }

    四.move和forward底层实现方式

    (一).move底层实现

    首先看一下move函数底层代码:

    1. template <typename T>
    2. typename remove_reference::type&& move(T&& t)
    3. {
    4. return static_case<typename remove_reference::type&&>(t);
    5. }

    其中参数T&&是万能引用,可接收左值或右值。

    返回值很特殊,typename remove_reference::type的含义就是去掉T的引用类型

    remove_reference本身是模板类,它的作用就是返回一个类型,所以这个类里面只有成员类型

    通过remove_reference源码可以看到,不管传入的是左值引用还是右值引用,它都只会返回这个值去掉引用后的类型。

    我们以int为例,不管传入int&还是int&&,经过remove_reference后,返回的都是int。

    1. template <typename T>
    2. struct remove_reference{
    3. typedef T type; //成员类型
    4. };
    5. template <typename T>
    6. struct remove_reference //左值引用
    7. {
    8. typedef T type;//返回T本身的类型
    9. }
    10. template <typename T>
    11. struct remove_reference //右值引用
    12. {
    13. typedef T type;//返回T本身的类型
    14. }

    static_case作用是强制类型转换,可以将左值强转成右值,move中是强转成右值引用。

    因此,move底层代码可以翻译成如下形式:

    1. template <typename T>
    2. int&& move(T&& t)
    3. {
    4. return (int&&)(t);
    5. }

    于是,我们清楚的发现:move函数就是通过remove_reference获取引用对象本身的类型,强转成右值引用的方式实现的

    (二).forward底层实现

    这是forward底层代码:

    1. template <typename T>
    2. T&& forward(typename std::remove_reference::type& param)//左值引用
    3. {
    4. return static_cast(param);//万能引用
    5. }
    6. template <typename T>
    7. T&& forward(typename std::remove_reference::type&& param)//右值引用
    8. {
    9. return static_cast(param);//万能引用
    10. }

     有了move的基础,forward就不难理解了。

    它通过remove_reference来区分传入的参数是左值引用还是右值引用,然后调用具体的重载forward函数。

    再通过万能引用的形式,根据param的具体类型返回左值引用还是右值引用。

    源码可以翻译成如下形式(int为例):

    1. template <typename T>
    2. T&& forward(int& param)//左值引用
    3. {
    4.     return (T&&)(param);//万能引用
    5. }
    6. template <typename T>
    7. T&& forward(int&& param)//右值引用
    8. {
    9.     return (T&&)(param);//万能引用
    10. }

    参考文章:

    聊聊C++中的完美转发 - 知乎 (zhihu.com)

    C++高阶知识:深入分析移动构造函数及其原理 | 音视跳动科技 (avdancedu.com)

    参考书籍:

    《C++ Primer》 

    程序是我的生命,但我相信爱她甚过爱我的生命。——未名


    如有错误,敬请斧正 

  • 相关阅读:
    C++&QT---QT-day3
    Ubuntu系统apt添加第三方PPA源
    Armbian 23.11(Ubuntu 22.04)安装glances不显示docker容器状态解决办法
    什么是Mybatis?Mybatis有什么作用?
    Jupyter Notebook出错提示An error occurred while retrieving package information解决办法
    LeetCode //C - 105. Construct Binary Tree from Preorder and Inorder Traversal
    [个人向x码农向]Acwing的springboot课程学习笔记(上)
    Effective C++ 阅读笔记 06:继承与面向对象设计(上)
    SpringBoot+Redis BitMap 实现签到与统计功能
    Vue3 <script setup>中局部引入组件,动态组件不渲染内容
  • 原文地址:https://blog.csdn.net/weixin_61857742/article/details/127877905