• 【C++11】C++11新增语法特性 右值引用/移动语义/完美转发


    1 右值引用

    1.1 左值 、 右值

    在C++中所有的值不是左值就是右值。左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。有名字的对象都是左值,右值没有名字。

    区分左值和右值最便捷的方法就是看能不能对表达式取地址。如果能,则为左值,否则就是右值。


    C++11扩展了右值的概念,将右值分为纯右值将亡值

    纯右值

    1.非引用返回的临时对象
    2.运算表达式产生的结果
    3.字面常量(C风格字符串除外,它是地址)

    将亡值

    与右值引用相关的表达式。
    例如:将要被移动的对象,T&&的函数返回值,std::move()的返回值,转换成T&&类型的转换函数的返回值

    左值右值代码示例:

    class A
    {
    public:
    	int a;
    };
    A getTemp()
    {
    	return A();
    }
    	int i = 3; //i是左值,3是右值
    	int j = i + 8;//j是左值,i+8是右值
    	A a1 = getTemp();//a1是左值,getTemp()返回的临时变量是右值引用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    1.2 左值引用 VS 右值引用

    在C++98中的引用是左值引用,就是给变量取个别名。在C++11中,因为新增了右值引用的概念,所有把C++98中的引用都称为左值引用。

    右值引用就是给右值取个名字
    语法:

    数据类型&& 变量名 = 右值;

    代码示例:

    #include 
    
    using namespace std;
    
    class A
    {
    public:
    	int m_a = 9;
    };
    A getTemp()
    {
    	return A();
    }
    int main()
    {
    	int&& a = 3;//3是右值
    	int b = 8;//b是左值
    	int&& c = b + 5;//b+5 是右值
    	A&& aa = getTemp();//getTemp的返回值是右值(临时变量)
    
    	cout << "a=" << a << endl;//3
    	cout << "c=" << c << endl;//13
    	cout << "aa.m_a=" << aa.m_a << endl;//9
    	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
    • 25

    值得一提的是,右值有了名字之后,就成了普通变量,普通变量有名字就可以取地址。
    所以上述代码中:
    a,c,aa均可以看成是左值

    我们可以用代码验证一下:
    对左值进行运算:

    	int&& a = 3;//3是右值
    	a++;
    	int b = 8;//b是左值
    	int&& c = b + 5;//b+5 是右值
    	c++;
    	A&& aa = getTemp();//getTemp的返回值是右值(临时变量)
    	aa.m_a++;
    	cout << "a=" << a << endl;//4
    	cout << "c=" << c << endl;//14
    	cout << "aa.m_a=" << aa.m_a << endl;//10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对左值取地址

    在这里插入图片描述

    也可以像普通引用一样加const约束

    在这里插入图片描述

    1.3 谈谈C++11引入右值引用的意义

    在上述代码中,getTemp()的返回值在表达式语句结束之后也就该终结了(因为是临时变量),而通过右值引用获得了新生,其生命周期将与右值引用aa的生命周期一样,只要aa还活着,该右值临时变量将会一直活下去。

    引入右值引用的主要目的是实现移动语义(后面会讲)

    1.4 左值引用和右值引用的一些细节问题

    左值引用只能绑定(关联、指向)左值,右值引用只能绑定(关联、指向)右值,如果绑定的不对,编译失败。

    错误代码示例:
    把上述代码的右值引用改为左值引用

    int& a = 3;//错误代码,3是右值
    int& c = b + 5;错误代码,b+5 是右值
    A& aa = getTemp();//,错误代码,getTemp的返回值是右值(临时变量)
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    错误代码示例:
    错误使用右值引用

    int&& c = b;//错误代码,b是左值,却用了右值引用
    
    • 1

    在这里插入图片描述


    万能引用类型 : 常量左值引用

    常量左值引用可以算是一个万能的引用类型,它可以绑定非常量左值,常量左值,右值,而且在绑定右值的时候,常量左值引用一样将右值的生命周期延长,缺点是只能读,不能改。

    在笔者之前写过一篇关于常量左值引用
    常量左值引用的一些经典问题

    在C++引入了右值引用以后,能更方便地解决了这些问题。

    代码示例:

    	int a = 1;
    	const int& ra = a;//a是非常量左值
    	const int b = 1;
    	const int& rb = b;//b是常量左值
    	const int& rc = 1;//1是右值
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2 移动语义

    如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
    深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。C++新增的移动语义就能够做到这一点。

    实现移动语义要增加两个函数:移动构造函数和移动赋值函数

    移动构造函数的语法:

    类名(类名&& 源对象){…}

    移动赋值函数的语法:

    类名&operator=(类名&& 源对象){…}

    代码示例:
    我们先写出不用右值引用的拷贝构造函数和赋值函数

    #include 
    #include 
    using namespace std;
    
    class AA
    {
    public:
    
    	AA(int* _m_data)
    		:m_data(new int[sizeof(_m_data)])//开辟空间
    	{
    		*m_data = *_m_data;//赋值
    	}
    	AA()
    	{
    	}
    	AA(const AA& a)//拷贝构造函数
    		:m_data(nullptr)
    	{
    		cout << "调用了拷贝构造函数" << endl;
    		AA tmp(a.m_data);
    		swap(tmp);
    	}
    	AA& operator=(const AA& a)
    	{
    		cout << "调用了赋值函数" << endl;
    		if (this != &a)//避免自我赋值
    		{
    			AA tmp(a.m_data);
    			swap(tmp);
    		}
    		return *this;
    	}
    	void swap(AA& a)
    	{
    		std::swap(m_data, a.m_data);//库里面的交换函数
    	}
    	~AA()
    	{
    		delete m_data;
    		m_data = nullptr;
    	}
    public:
    	int* m_data = nullptr;//数据成员,指向堆区资源的指针
    };
    
    int main()
    {
    	int t = 3;
    	AA a1(&t);//创建一个对象a1
    	cout << "a1.m_data=" << *a1.m_data << endl;
    
    	AA a2 = a1;//将调用拷贝构造函数
    	cout << "a2.m_data=" << *a2.m_data << endl;
    
    	AA a3;
    	a3 = a1; //将调用赋值函数
    	cout << "a3.m_data=" << *a3.m_data << endl;
    	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
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    在这里插入图片描述

    其中我们拷贝构造函数和赋值函数采用了现代写法:先创建一个临时对象,临时对象会调用一次次有参构造,申请内存资源,然后将该临时对象与this指针交换即可完成我们的拷贝构造。赋值函数同理。

    不管是传统的写法还是现代的写法,都需要再申请一份堆区的资源,这份申请的资源被拷贝完后就没什么用了,造成了资源的浪费。于是就有了我们的移动语义

    AA(const AA& a)//拷贝构造函数
    		:m_data(nullptr)
    	{
    		cout << "调用了拷贝构造函数" << endl;
    		AA tmp(a.m_data);
    		swap(tmp);
    	}
    	AA(AA&& a)  //移动构造函数
    		:m_data(nullptr)       
    	{
    		cout << "使用了移动语义" << endl;
    		swap(a);//直接交换即可,不用再申请空间
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    注意:移动构造函数的函数参数里不能加const,因为我们要对a这个对象的值进行修改。

    这是交换前:
    在这里插入图片描述
    交换后:
    在这里插入图片描述
    赋值函数也同理:

    AA& operator=(const AA& a) //赋值函数
    	{
    		cout << "调用了赋值函数" << endl;
    		if (this != &a)//避免自我赋值
    		{
    			AA tmp(a.m_data);
    			swap(tmp);
    		}
    		return *this;
    	}
    	AA& operator=(AA&& a)//移动赋值函数
    	{
    		cout << "调用了移动语义" << endl;
    		if (this != &a)//避免自我赋值
    		{
    			swap(a);
    		}
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们运行一下代码试试:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    注意事项:
    1.对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?(就是我们的演示代码)C++11为了解决这个问题,提供了std::move()方法来讲左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我使用拷贝函数,而是用移动构造函数。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
    2.如果没有提供移动构造/移动赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/移动赋值函数就去寻找拷贝构造/赋值函数。
    3.C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
    4.移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。

    3 完美转发

    在讲完美转发之前,我们先来看这样一个问题。

    如下的代码,我们想要的结果是让fun(a)调用fun1(int& ii) 这个函数,fun(8)调用fun1(int&& ii)这个函数,但是通过一个函数中转调用,编译器都会认为是左值。

    void fun1(int& ii) //参数如果是左值,调用此函数
    {
    	cout << "参数是左值=" << ii << endl;
    }
    void fun1(int&& ii) //参数如果是右值,调用此函数
    {
    	cout << "参数是右值=" << ii << endl;
    }
    
    void fun(int i) //通过该函数调用fun1
    {
    	fun1(i);
    }
    int main()
    {
    	int a = 3;
    	fun(a);//a是左值
    	fun(8);//8是右值
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    那如果我们把代码改成这样:

    void fun(int& i) //通过该函数调用fun1
    {
    	fun1(i);
    }
    
    • 1
    • 2
    • 3
    • 4

    fun(8)这行代码就会报错,因为8是右值
    在这里插入图片描述
    同理,把代码改成这样:

    void fun(int&& i) //通过该函数调用fun1
    {
    	fun1(i);
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    无法将左值绑定到右值引用

    解决方案
    我们重载一份fun函数,一份左值引用,一份右值引用。
    右值引用的的fun()在调用fun1的时候,把i利用move函数改成右值

    void fun(int& i) //通过该函数调用fun1
    {
    	fun1(i);
    }
    void fun(int&& i) //通过该函数调用fun1
    {
    	fun1(move(i));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果上述代码是以函数模板的形式写的,那么C++11有了更完美的解决方案,就是完美转发
    原代码

    template <class T> 
    void fun(T& i) //通过该函数调用fun1
    {
    	fun1(i);
    }
    template <class T>
    void fun(T&& i) //通过该函数调用fun1
    {
    	fun1(move(i));
    }
    int main()
    {
    	int a = 3;
    	fun(a);//a是左值
    	fun(8);//8是右值
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    在这里插入图片描述
    上述的重载了两个函数模板,反而违背了模板的理念。所以,利用C++11完美转发优化后的代码如下:

    void fun1(int& ii) //参数如果是左值,调用此函数
    {
    	cout << "参数是左值=" << ii << endl;
    }
    void fun1(int&& ii) //参数如果是右值,调用此函数
    {
    	cout << "参数是右值=" << ii << endl;
    }
    
    template <class T>
    void fun(T&& i) //注意这里:左值为什么也能绑定到右值?
    {
    	fun1(forward<T>(i));//完美转发
    }
    int main()
    {
    	int a = 3;
    	fun(a);//a是左值
    	fun(8);//8是右值
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    在函数模板中,可以将参数“完美”的转发给其他函数。所谓完美,即不仅能准确的转发参数的值,还能保住被转发参数的左、值属性不变。
    C++11标准引入了右值引用和移动语义,所以,能否实现完美转发,决定了该参数在传递过程中使用的是拷贝语义还是移动语义。
    为了支持完美转发,C++11提供了以下方案:

    引用折叠(模板参数才能使用)

    如果模板中(包括类模板和函数模板)函数的参数书写成为T&&,那么,函数既可以接受左值引用,又可以接受右值引用。

    std::forward 参数

    提供了函数模板 std::forward(参数),用于转发参数,如果参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。

    4 总结

    以上就是右值引用/移动语义/完美转发的解释。C++11引入引入右值引用是为了给移动语义和完美转发服务的。移动语义的移动拷贝和移动赋值为我们避免了内存资源的浪费,完美转发解决了代码的冗余。

  • 相关阅读:
    【数据结构】手撕排序算法(中)交换排序 (冒泡排序、快速排序的递归方式(挖坑法、前后指针法、左右指针法))、归并排序的递归方式
    导航【C++】
    【VulnHub靶场】——BOREDHACKERBLOG: SOCIAL NETWORK
    java面试题整理《微服务篇》四
    mapbox 地图 生成矢量数据圆
    Python转换文件夹中的图片格式
    Win11勒索软件防护怎么打开?Win11安全中心勒索软件防护如何设置
    VSCode远程连接Linux
    nmap之nse脚本简单学习
    谷粒商城 高级篇 (十一) --------- Spring Cache
  • 原文地址:https://blog.csdn.net/iamxiaobai_/article/details/130892543