• C++11 右值,右值引用,移动构造,移动赋值


    目录

    一、左值,左值引用,右值,右值引用的相关概念:

    1. 什么是左值,什么是左值引用?

    2. 什么是右值,什么是右值引用?

    3. 右值的属性是右值,右值引用的属性是左值

    4. 左值引用与右值引用的简单比较:

    二、右值引用的作用(使用场景)

    左值引用的作用:

    左值引用的短板:

    右值引用作用1,使用场景1:

    to_string传值返回,string类的拷贝构造 vs 移动构造

    to_string传值返回,string类的拷贝赋值 vs 移动赋值

    场景一总结:

    右值引用作用2,使用场景2:

    三、模板中的万能引用&&  与  完美转发


    一、左值,左值引用,右值,右值引用的相关概念:

    1. 什么是左值,什么是左值引用?

    左值是一个表示数据的表达式(如变量名或解引用的指针),左值的特征是可以获取它的地址 + 可以对它赋值,左值可以出现在赋值符号的左边。(右值不能出现在赋值符号的左边)。
    有一个例外:const修饰的左值不能出现在赋值符号左边。
    故,左值最大的特点是是:可以对它取地址。

    左值引用就是对左值的引用,给左值取别名。

    1. // 以下的p,b,c,*p都是左值
    2. int* p = new int(0);
    3. int b = 1;
    4. const int c = 2;
    5. *p = 10;
    6. // 左值引用:
    7. int*& rp = p;
    8. int& rb = b;
    9. const int& rc = c;
    10. int& ri = *p;

    2. 什么是右值,什么是右值引用?

    右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回,也就是必须是值返回)... 
    右值不能出现在赋值符号的左边,可以出现在赋值符号的右边,右值不能取地址。
    右值引用就是对右值的引用,给右值取别名。
    赋值运算符的左侧必须为左值。“=”: 左操作数必须为左值。

    右值可以分为两种:1. 内置类型右值-纯右值    2. 自定义类型右值-将亡值

    1. double x = 10.1, y = 20.2;
    2. // 右值示例:
    3. 10;
    4. x+y;
    5. fmin(x, y); // 值返回
    6. // 右值引用示例:
    7. int&& ri = 10;
    8. double&& rd = x + y;
    9. double&& rd2 = fmin(x, y);

    3. 右值的属性是右值,右值引用的属性是左值

    右值不能取地址,比如10不能取地址。但是右值引用可以取地址,且可以给右值引用赋值。也就是 int&& ri = 10; &ri; ri = 20;  是合法的。因为给右值取别名后,会导致右值引用被存储在特定位置,属性变为左值。

    右值引用的属性是左值。const 右值引用的属性是const 左值。因为左值可以取地址,可以赋值。因此右值引用可以取地址,可以赋值。const 右值引用可以取地址,不能赋值。

    4. 左值引用与右值引用的简单比较:

    1. 左值引用只能引用左值,不能引用右值。 const 左值引用既能引用左值,也能引用右值。

    2. 右值引用只能引用右值,不能引用左值。 但是右值引用可以引用move后的左值。(这个其实就是move函数的作用,std::move函数可以将左值强制转化为右值引用,返回这个左值的右值引用)

    二、右值引用的作用(使用场景)

    既然左值引用可以引用左值,且const 左值引用还可以引用右值,那么右值引用的作用是什么呢?探究C++11引入右值引用的作用之前,先来探究左值引用的作用,以及左值引用的短板,从而理解右值引用的作用。

    左值引用的作用:

    1. 左值引用做函数参数。

    a. 引用传参,减少拷贝,提高效率。最典型的比如拷贝构造函数,operator=(),以对象为模板拷贝构造一个新的对象,这里传引用可以减少传参时的拷贝,提高效率。
    b. 做输出型参数,代替C语言的传指针。

    2. 左值引用做函数返回值。

    a. 传引用做返回值,减少拷贝,提高效率。(我们知道,传值返回是要拷贝构造一个临时对象的(编译器不优化或不能优化的情况下),这里会降低效率。)
    b. 传引用返回,用于修改返回对象。最典型的比如 vector的operator[]。很多堆区开辟的对象,都可以采用传引用返回的方式,使程序使用对一个对象进行处理。

    重新审视拷贝构造函数和重载赋值运算符函数的参数,为什么要定为 const Type& t   一方面,传引用可以提高效率,减少拷贝。这里加const的作用可以防止修改左值引用。另一方面是,const 左值引用才能引用右值。否则这个拷贝构造函数或operator=不能以右值为实参。也就是 T t(T());  将报错,因为左值引用不能引用临时对象的这种右值。

    左值引用的短板:

    若一个对象在堆区,则函数可以传引用返回。但是,对于函数内的局部临时对象,函数返回后,出了作用域,局部对象就会销毁,是不可以传引用返回的。
    而如果采用值返回,则会发生拷贝构造,若这个对象不是内置类型,而是深拷贝的自定义类型,则发生深拷贝还会降低效率。 对于自定义类型的右值,我们称之为将亡值,也就是函数内的局部临时对象。

    右值引用作用1,使用场景1:

    C++11中,右值引用的一个重要功能,就是解决函数值返回涉及深拷贝的局部临时对象的低效率问题。解决方法为:利用右值引用为自定义类型实现移动构造和移动赋值成员函数。

    to_string传值返回,string类的拷贝构造 vs 移动构造

    比如,我们模拟实现一个string类。则,yzl::string to_string(int val); 函数,用于将整型转为我们实现的string类型,此处必须传值返回,因为函数内创建的局部string对象出了函数作用域就会销毁。

    1. yzl::string to_string(int val)
    2. {
    3. yzl::string str;
    4. // ...
    5. return str; // 此str的属性为右值
    6. }
    7. void func()
    8. {
    9. yzl::string s = yzl::to_string(10);
    10. }
    11. // 以模拟实现string类为例的移动构造和移动赋值实现。
    12. // 移动构造
    13. string(string &&s)
    14. : _str(nullptr), _size(0), _capacity(0) {
    15. cout << "string(string&& s) -- 移动语义" << endl;
    16. swap(s);
    17. }
    18. // 移动赋值
    19. string &operator=(string &&s) {
    20. cout << "string& operator=(string&& s) -- 移动语义" << endl;
    21. swap(s);
    22. return *this;
    23. }

    若,我们不使用右值引用在yzl::string类内实现移动构造和移动赋值,只有拷贝构造和拷贝赋值。则这里在编译器未优化的情况下,要进行两次拷贝构造。若编译器对此情况进行优化,则进行一次拷贝构造。(to_string的str局部对象是右值属性,但是因为const &可以接收右值,所以调用拷贝构造)

    若,yzl::string类或者其他深拷贝的自定义类型,实现了移动构造。这里的str作为局部对象值返回时,它的性质为右值,所以就会调用参数更匹配的移动构造。

    若编译器不进行优化,则会调用两次移动构造函数,因为str 和 str构造出的临时对象都为右值。
    若编译器进行优化,则只会调用一次移动构造函数。

    注意:这里编译器的优化行为不是重点。

    重点是,对于这种涉及深拷贝的局部对象的函数返回值,属性为右值,这种对象在函数执行结束,出了函数作用域后就会销毁,若没有移动构造或移动赋值。则就会进行深拷贝的拷贝构造。但是,对于这种将亡值,是没必要进行深拷贝的,我们可以直接将其资源进行转移。方式就是调用移动构造。


    to_string传值返回,string类的拷贝赋值 vs 移动赋值

    上方举例时,使用的是拷贝构造函数和移动构造函数,也就是函数值返回用于构造一个新的对象。

    赋值的情况同理,若函数返回值用于赋值给一个同类对象,且这个类还没有实现移动赋值,则就会调用深拷贝的拷贝赋值,即string& operator=(const string& s); (注意,const左值引用可以引用右值)  对于这种局部对象的将亡值,没必要进行深拷贝赋值。可以实现 string& operator=(string && s);  移动赋值。即可减少深拷贝,提高效率。

    1. yzl::string to_string(int val)
    2. {
    3. yzl::string str;
    4. // ...
    5. return str; // 此str的属性为右值
    6. }
    7. void func()
    8. {
    9. // 赋值场景
    10. yzl::string s("hahaha");
    11. s = yzl::to_string(10);
    12. }

    暂不探究这里的编译器优化行为,若不进行优化。

    若没有实现移动构造和移动赋值,则会先进行拷贝构造,然后拷贝赋值,都是以一个右值将亡值为参数,进行深拷贝,这显然是没必要的且低效的。若实现了移动构造和移动赋值,则这里会先进行移动构造,再进行移动赋值。

    场景一总结:

    右值引用的使用场景1,指的是当函数局部对象进行值返回时,不能采用左值引用返回,若不利用右值引用实现出移动构造和移动赋值,则对一个将亡值进行深拷贝效率低下。

    这里并不是直接将返回值设为右值引用,而是利用右值引用实现出移动构造和移动赋值。减少对将亡值右值的深拷贝。

    总结就是,传值返回深拷贝类型对象时,拷贝构造和移动构造可提高效率。

    右值引用作用2,使用场景2:

    STL中的vector,list等容器在C++11之后,都实现了右值引用版本的push_back,等插入函数。
    函数调用时,若实参是一个右值对象,则插入函数内部会进行资源移动,减少拷贝(深拷贝)。若实参是一个左值对象,则插入函数内部会进行拷贝构造,拷贝赋值等深拷贝操作。

    1. void haha2()
    2. {
    3. list ls;
    4. yzl::string s1("haha");
    5. ls.push_back(s1);
    6. cout << endl << endl;
    7. ls.push_back("hehe");
    8. ls.push_back(yzl::string("hehe"));
    9. ls.push_back(std::move(s1));
    10. }

     对于不同属性的实参,调用形参类型最匹配的函数,对于右值,将亡值类型,调用移动构造或移动赋值等减少深拷贝,提高效率的类成员函数。

    三、模板中的万能引用&&  与  完美转发

    1. 类模板或函数模板中的&&,不代表右值引用,而是万能引用。
    2. 其既能接收左值,也能接收右值(万能)
    3. 模板的万能引用只是提供了同时能够接收左值引用和右值引用的能力
    4. 不管万能引用接收的是左值还是右值,这个万能引用都是左值属性!(其实,左值引用本身就是左值属性,而上面说过右值引用也会是左值属性。)
    5. 若我们希望在传递过程中保持原对象的左值或右值属性,就需要使用完美转发,
    std::forward(x); std::forward(x)在传参的过程中保持了t的原生类型属性。

    1. void Fun(int &x) { cout << "左值引用" << endl; }
    2. void Fun(const int &x) { cout << "const 左值引用" << endl; }
    3. void Fun(int &&x) { cout << "右值引用" << endl; }
    4. void Fun(const int &&x) { cout << "const 右值引用" << endl; }
    5. // std::forward(t)在传参的过程中保持了t的原生类型属性。
    6. template <typename T>
    7. void PerfectForward(T &&t) // 这里的万能引用,不是右值引用。普通的右值引用不能接收左值。
    8. {
    9. Fun(t);
    10. }
    11. int test1()
    12. {
    13. PerfectForward(10); // 右值
    14. int a;
    15. PerfectForward(a); // 左值
    16. PerfectForward(std::move(a)); // 右值
    17. const int b = 8;
    18. PerfectForward(b); // const 左值
    19. PerfectForward(std::move(b)); // const 右值
    20. return 0;
    21. }
    22. int main()
    23. {
    24. test1();
    25. return 0;
    26. }

     若在万能引用的传参过程中,使用std::forward(t); 则会保持t的原生属性。

    我们知道,STL中的容器,都是用模板实现的,里面在C++11之后,一些函数实现了右值引用版本,而STL容器具体实现是很复杂的,层层调用,里面很多地方都会使用完美转发。

    若我们自己使用模板模拟实现STL容器,实现右值引用版本的成员函数时,也必须使用完美转发,具体就不举例了。

  • 相关阅读:
    使用Spring Cache实现广告缓存并基于RabbitMQ实现双写一致
    WebAssembly学习记录
    Linux学习之HIS部署(5)
    SpringMVC之框架搭建&开发实例&请求的处理流程
    DVBS 卫星波段 设置
    「AI知多少」第二期推荐《AIGC:智能创作时代》
    Linux下kibana的安装与配置
    .NET/C#汇总 —— 数据库概念知识
    第十六章总结:反射和注解
    【买入看跌期权策略(Long Put)】
  • 原文地址:https://blog.csdn.net/i777777777777777/article/details/128051018