• 【C++11】右值引用、移动构造、移动赋值、完美转发 的原理介绍



    传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以我们管之前的引用叫做左值引用。无论左值引用还是右值引用,都是给对象取别名


    一、概念

    1.1 左值

    • 左值是一个表示数据的表达式(如变量名或解引用的指针),通俗的理解就是,能取到地址的就是左值

    • 正常情况下我们可以对左值赋值,定义 const 修饰后的左值,不能给它赋值,但是可以取出它的地址;

    • 左值可以出现在赋值符号 " = " 的左边,也可以出现在赋值符号的右边;

    • 左值具有持久的状态。

    // 常见的左值
    int a = 0;
    int b = 1;
    int* p = &a;
    const int c = 3;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.2 左值引用

    • 左值引用:就是对左值的引用,相当于给左值取别名。
    • 左值引用只能引用左值,不能直接引用右值;但是 const 左值引用可以引用右值。
    • 左值引用符号 &
    // 左值引用给左值取别名
    int& ref1 = a;
    
    // 左值引用给右值取别名
    // int& ref2 = (a + b);			// err...(a+b)返回的是临时对象,临时对象具有常性,出现权限放大问题
    const int& ref2 = (a + b);		// 加 const 就行了
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    1.3 什么是右值?

    • 右值也是一个数据表达式,右值是 字面常量 或者是 求值过程种创建的临时对象

    • 对于右值,不能取出地址,不能对它赋值

    • 右值的生命周期是短暂的,如:字面常量,表达式返回值,函数返回值(不是左值引用的返回值),临时变量,匿名对象…;

    // 右值
    a + b;
    func(x,y);
    10;
    "abcd";
    
    • 1
    • 2
    • 3
    • 4
    • 5

    1.4 什么是右值引用?

    • 右值引用相当于给右值起别名,右值引用只能右值,除非左值 move 后,可以对其进行右值引用
    • 右值是没有地址的,但是右值引用后,这个右值引用会被存到特定的位置,且可以取到该值的地址,也就是说 右值引用值是一个左值
    • 右值引用会开辟一块空间去存右值,普通的右值引用可以修改这块空间,const 的右值引用则不可以被修改。
    • 右值引用符号 &&

    move():标准库中的函数,可以将左值强制转换为右值

    // 右值引用给右值取别名:
    int&& ref3 = (a + b);			
    
    // 右值引用不能给左值起别名:
    //int&& ref4 = a;		// err
    
    // 右值引用可以 给 move 后的左值取别名:
    int&& ref4 = move(a);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对于参数左值还是右值的不同,是被重载支持的

    //void func(const int& a)	// 这样虽说都可以使用,但是区分不了左值和右值
    void func(int& a)
    {
    	cout << "void func(int& a)" << endl;
    }
    
    void func(int&& a)
    {
    	cout << "void func(int&& a)" << endl;
    }
    
    int main()
    {
    	int a = 0;
    	int b = 1;
    	func(a);
    	func(a + b);
    	return 0;
    }
    ----------------------------------
    输出结果:
    void func(int& a)
    void func(int&& a)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    左值引用的使用场景 和 缺陷

    左值引用可以直接减少拷贝。应用如下:

    1. 左值引用传参;
    2. 传引用返回。

    左值引用解决了大多数场景的问题,但也存在一些解决不了的问题:

    1. 局部对象返回问题;
    2. 对象深拷贝问题。

    C++11 前,对于带指针的容器(比如 string)会进行深拷贝,深拷贝的目的是让赋值和被赋值的对象,都正常保存并使用数据互不影响,代价比较高。而如果被拷贝对象是右值,右值本不需要留存浅拷贝就可以,但还是进行了深拷贝,是很不划算的。

    ttang::string s1("hello world");
    
    ttang::string ret1 = s1;		// 左值拷贝
    ttang::string ret2 = (s1+'!');	// 右值拷贝,如果也是深拷贝,很不划算
    // 实际上右值拷贝,C++11使用的是移动拷贝构造,是浅拷贝
    // 可以很大程度的优化右值的拷贝效率
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    二、移动语义

    2.1 移动拷贝构造

    移动拷贝构造函数跟构造函数一样,参数需要是一个本身类型的对象,但 移动拷贝构造函数的参数是一个该类型的右值引用

    所谓移动,是数据交换。移动拷贝函数创建出一个新对象,将新对象中的值都设置为 0,接下来与传进来的右值对象进行资源交换

    // 移动拷贝构造
    string(string&& s)
    	:_str(nullptr),
    	_size(0),
    	_capacity(0)
    {
    	swap(s);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 根据函数匹配规则,如果调用拷贝构造对象的时候传的是左值,编译器会自动匹配到拷贝构造函数,如果传的是右值,那么就会匹配到移动拷贝函数。

    • 使用移动拷贝构造函数后,源对象指向资源就被交换出去,这些资源的所有权都归属到了新对象。
      因此,如果源对象是一个长期存在的对象的时候,需要谨慎使用移动拷贝构造函数。调用移动拷贝构造函数创建出 s3,s1 的资源被转移到了 s3,s1 中没有指向任何资源,所以就不能通过 s1 去寻找之前的资源。

    • 如果只是调用 move() 函数,而不对其返回值进行接收,是不会改变传入值的内容的。

    std::string s1("hello");
    
    // 拷贝
    std::string s2 = s1;
    // 移动(s3 里面存了“hello”,而 s1 空了)
    std::string s3 = move(s1);
    
    // 只是这样不接受其返回值,是不会改变s2的
    move(s2);
    std::string s4 = s2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    一些使用举例:

    list<ttang::string> lt;
    
    ttang::string s1("hello world");		
    lt.push_back(s1);							// 深拷贝
    lt.push_back(move(s1));						// 移动
    
    lt.push_back(ttang::string("hello world"));	// 移动,匿名对象也是右值
    lt.push_back("hello world");				// 移动
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    总的看来:

    • 左值引用减少拷贝,右值引用也是减少拷贝,提高效率;
    • 但是他们的角度不同,左值引用是直接减少拷贝;右值引用是间接减少拷贝,通过函数重载,识别出是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝,提高效率。

    2.2 移动赋值

    对于初始化:

    • 如果 string 类中只有一个移动拷贝构造函数,那么函数返回值构造临新对象的时候,那么只需要调用一次 移动拷贝构造 函数将资源转移给新对象。

    对于已经初始化过的对象进行赋值:

    • 那么就会调用一次 移动拷贝构造 函数和一次 赋值重载 函数,赋值重载函数也是进行深拷贝的。

    因此,为了解决赋值重载的深拷贝的问题,我们还需要再实现一个移动赋值重载函数,移动赋值重载 函数跟拷贝构造函数一样,都是 解决深拷贝的问题,都是进行转移资源

    移动赋值重载函数跟赋值重载函数的定义是类似的,只是移动赋值重载函数的参数是右值引用,是为了让右值能够调用该函数,如下:

    // 移动赋值
    string& operator=(string&& s)
    {
    	swap(s);
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    三、右值引用 与 STL

    综上推演,C++11 为很多容器都增加了 移动拷贝构造函数赋值重载函数的右值引用版本,包括 push_back 或者 insert 接口也增加了右值引用 的重载

    3.1 移动拷贝构造 和 赋值重载

    在这里插入图片描述
    在这里插入图片描述

    3.2 插入接口

    在这里插入图片描述
    在这里插入图片描述

    上面的文档中,insert 和 push_back 可以接收 const 左值引用,也就是说这个函数既可以接收左值,也可以接受右值,那么为什么还需要多定义一个参数是右值引用的函数重载呢?

    • 因为,我们说了左值引用可以接收右值但是相应的拷贝任然是深拷贝,重载右值引用版本正是为了优化这一点。
    • 再梳理一下传入右值的流程:如果调用 vector 中 insert 时,传的参数是右值,那么就会编译器就会匹配到右值引用参数的 insert 的重载函数,因为 insert 函数内部会对该参数值进行赋值重载到 vector 内,如果是右值,那么就会调用移动拷贝构造函数,所以可以避免深拷贝的出现。
    vector<string> v;
    string str("hello");
    v.insert(v.end(), str);	// str 是左值,深拷贝
    v.insert(v.end(), string("hello~"))	// string("xx") 是匿名对象,调用右值版本
    
    • 1
    • 2
    • 3
    • 4
    • 因此,我们在赋值、构造、以及调用 insert、push_back、时,如果涉及深拷贝的问题,尽量传右值(匿名对象),这样可以减少深拷贝的问题。

    3.3 完美转发、万能引用

    完美转发

    关于右值引用本身的属性,举例:

    int&& a = 10;
    cout << &a << endl;	// a 可取地址
    a++;				// a 可修改
    
    右值引用的 a,虽然是通过右值得到的,但 a 本身是左值!!!
    
    • 1
    • 2
    • 3
    • 4
    • 5

    进一步举例:

    void insert(iterator pos, const T&& val)
    {
    	//...
    	*pos = val;	// val 是左值,调用的是赋值重载
    	++_end;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    很明显的,在上面调用参数是右值引用的 insert 接口中存在一个问题,如果右值引用 val 去接受一个右值,那么这个 val 就会退化成一个左值。所以 *pos = val; 这一步调用的还是重载函数,不是移动拷贝构造。因此,为了保持 val 是一个右值,有一个专门的写法:

    std::forward(val):它可以在传参的时候保持 val 原生属性,也就是可以保持其右值属性,因此,这样可以保证 *pos = val; 调用的会是移动赋值重载函数。

    这种调用 std::forward(val),使得 val 保持原生属性的过程就是 完美转发,写成如下:

    void insert(iterator pos, const T&& val)
    {
    	//...
    	*pos = std::forward<T>(val);	// 保持了 val 的右值属性!调用的就是其移动赋值函数
    	++_end;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    万能引用

    万能引用 又叫 引用折叠,使用 && 进行定义或申明。万能引用定义的参数,即可以对左值引用,也可以对右值引用。

    使用场景:函数模板的形参auto 声明

    template<typename T> 
    void f(T&& param);   //param是个万能引用
    
    auto && var2 = var1;
    
    • 1
    • 2
    • 3
    • 4

    以上两种场景的共同之处,在于它们都涉及类型推导。

    下面这两种只是普通的右值引用:

    template<typename T> 
    void f(std::vector<T>&& param); // param 是右值引用
    
    template<typename T> 
    void f(const T&& param);   		// 加 const 则是右值引用
    
    • 1
    • 2
    • 3
    • 4
    • 5

    对于万能引用和右值引用的区分,不必过多纠结~ 能用就行。

    void Fun(int& x) { cout << "左值引用" << endl; }
    void Fun(const int& x) { cout << "const 左值引用" << endl; }
    
    void Fun(int&& x) { cout << "右值引用" << endl; }
    void Fun(const int&& x) { cout << "const 右值引用" << endl; }
    
    template<typename T>
    void PerfectForward(T&& t)	// 这里面用这个右值转化的左值 t 可以,但是再转一层就会出问题
    {
    	Fun(forward<T>(t));		// 不加完美转发的话,会全是左值引用!!!
    }
    
    int main()
    {
    	int a;
    	PerfectForward(a);            // 左值
    	PerfectForward(std::move(a)); // 右值
    	PerfectForward(10);           // 右值
    	const int b = 8;
    	PerfectForward(b);		      // const 左值
    	PerfectForward(std::move(b)); // const 右值
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    🥰如果本文对你有些帮助,请给个赞或收藏,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 欢迎评论留言~~


  • 相关阅读:
    SLAM中旋转向量(旋转轴/旋转角)、旋转矩阵、四元数、李代数的相互转化(附C++ Eigen库代码实例)
    【JDK 8-集合框架进阶】6.3 foreach
    秦腔《花脸》首度加入游戏音乐盒,CSGO让全球玩家聆听“中国声音” 你想成为那个传颂者吗?CSGO开服搭建计划
    学生信息管理模块设计与开发
    Spring boot 启动流程及外部化配置
    JVM基础知识(一)jvm内存结构
    具体项目下解决Echarts多端同步开发和维护的问题
    如何将本地 PDF 文件进行翻译
    使用 cephadm 部署 aio ceph
    探索数据库,中文字段排序,到底是什么
  • 原文地址:https://blog.csdn.net/m0_67470729/article/details/133920525