• 【C++练级之路】【Lv.22】C++11——右值引用和移动语义




    快乐的流畅:个人主页


    个人专栏:《算法神殿》《数据结构世界》《进击的C++》

    远方有一堆篝火,在为久候之人燃烧!

    引言

    关于C++11的final和override的知识,在之前已经提到过,这里不再赘述,有需要的请移步这篇博客【C++练级之路】【Lv.13】多态(你真的了解虚函数和虚函数表吗?)

    一、右值引用

    1.1 左值和右值

    • 左值:可取地址,可在等号左右
    • 右值:不可取地址,只能在等号右边
    void test()
    {
    	int a;//左值
    	10;//右值
    	10 + 20//右值
    }
    

    一般情况下,左值均为变量名,而右值则为字面常量、表达式等。

    1.2 左值引用和右值引用的范围

    void test()
    {
    	int& ref1 = a;//左值引用,可以引用左值
    
    	//int& ref2 = a + b;//左值引用,不能引用右值(权限放大)
    	const int& ref2 = a + b;//const左值引用,可以引用右值
    
    	int&& ref3 = a + b;//右值引用,可以引用右值
    
    	//int&& ref4 = a;//右值引用,不能引用左值
    	int&& ref4 = move(a);//右值引用,可以引用move后的左值
    }
    
    • 左值引用,可以引用左值
    • const左值引用,可以引用右值
    • 右值引用,可以引用右值
    • 右值引用,可以引用move后的左值

    ps:move的作用,是将左值强制转换为右值引用,详情见move章节。
    ps:右值的引用属性为左值,将右值引用后,右值会被存储起来,并可以取到地址。

    1.3 左值引用的意义

    左值引用:

    1. 传引用传参,减少拷贝
    2. 传引用返回,减少拷贝(限制:函数内的局部对象,不能传引用返回)

    左值引用已经解决了绝大多数拷贝问题,但是唯一的缺陷就是不能传引用返回局部对象。所以,这就是右值引用存在的意义,为了补全这块不足。

    而要完全理解右值引用的意义,则需要学习移动语义,理解右值引用是如何减少拷贝的。

    二、移动语义

    首先,给出一个自己实现的精简版string类,方便调试和观察内部细节。

    namespace my
    {
    	class string
    	{
    	public:
    		typedef char* iterator;
    
    		iterator begin()
    		{
    			return _str;
    		}
    
    		iterator end()
    		{
    			return _str + _size;
    		}
    
    		string(const char* str = "")
    			: _size(strlen(str))
    			, _capacity(_size)
    		{
    			//cout << "string(char* str)" << endl;
    			_str = new char[_capacity + 1];
    			strcpy(_str, str);
    		}
    
    		void swap(string& s)
    		{
    			std::swap(_str, s._str);
    			std::swap(_size, s._size);
    			std::swap(_capacity, s._capacity);
    		}
    
    		// 拷贝构造
    		string(const string& s)
    			:_str(nullptr)
    		{
    			cout << "string(const string& s) -- 深拷贝" << endl;
    			string tmp(s._str);
    			swap(tmp);
    		}
    
    		// 赋值重载
    		string& operator=(const string& s)
    		{
    			cout << "string& operator=(string s) -- 深拷贝" << endl;
    			string tmp(s);
    			swap(tmp);
    			return *this;
    		}
    
    		~string()
    		{
    			delete[] _str;
    			_str = nullptr;
    		}
    
    		void reserve(size_t n)
    		{
    			if (n > _capacity)
    			{
    				char* tmp = new char[n + 1];
    				strcpy(tmp, _str);
    				delete[] _str;
    				_str = tmp;
    				_capacity = n;
    			}
    		}
    
    		void push_back(char ch)
    		{
    			if (_size >= _capacity)
    			{
    				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
    				reserve(newcapacity);
    			}
    			_str[_size] = ch;
    			++_size;
    			_str[_size] = '\0';
    		}
    
    		string& operator+=(char ch)
    		{
    			push_back(ch);
    			return *this;
    		}
    
    		string operator+(char ch)
    		{
    			string tmp = *this;
    			tmp += ch;
    			return tmp;
    		}
    	private:
    		char* _str;
    		size_t _size;
    		size_t _capacity; // 不包含最后做标识的\0
    	};
    }
    

    operator+是我们要重点观察的函数,特意拿出来方便对比:

    string operator+(char ch)
    {
    	string tmp = *this;
    	tmp += ch;
    	return tmp;
    }
    

    2.1 移动构造

    先来看看以下代码:

    void test()
    {
    	my::string s1 = "hello";
    	my::string s2 = s1 + '!';
    }
    

    在以往的经验中,operator+中的tmp(原始对象)传值返回,先拷贝构造给临时对象,tmp在函数域内销毁,然后临时对象再拷贝构造给s2(目标对象),总共有两次深拷贝。

    但是,如果运用上右值引用的移动构造,加上以下代码:

    // 移动构造
    string(string&& s)
    	: _str(nullptr)
    {
    	cout << "string(string&& s) -- 移动" << endl;
    	swap(s);
    }
    

    此时,operator+中的tmp(原始对象)传值返回,直接和s2互换资源(称之为移动),就可以直接无拷贝返回,直接减少了两次深拷贝。


    ps:如果符合编译器优化,编译器会自动将tmp识别为左值,从而将连续三次拷贝构造优化成一次。(VS2022)
    ps:如果不符合优化,编译器才会将tmp强制识别为右值,从而符合移动语义。
    ps:如果只有const&,右值会匹配;如果有&&,右值则会匹配更适合的。


    2.2 移动赋值

    同理,再看看这段代码:

    void test()
    {
    	my::string s1 = "hello";
    	my::string s2;
    	s2 = s1 + '!';
    }
    

    在以往的经验中,operator+中的tmp(原始对象)传值返回,先拷贝构造给临时对象,tmp在函数域内销毁,然后临时对象再赋值给s2(目标对象),总共有两次深拷贝。

    但是,如果运用上右值引用的移动赋值,加上以下代码:

    // 移动赋值
    string& operator=(string&& s)
    {
    	cout << "string& operator=(string&& s) -- 移动" << endl;
    	swap(s);
    	return *this;
    }
    

    此时,operator+中的tmp(原始对象)传值返回,直接和s2互换资源(称之为移动),就可以直接无拷贝返回,直接减少了两次深拷贝。

    ps:此时不是连续的拷贝构造,而是拷贝构造+赋值,所以编译器不会优化。

    2.3 右值引用的意义

    对于函数内的右值,分为两类:

    • 纯右值:内置类型的右值
    • 将亡值:自定义类型的右值

    右值引用:把将亡值的资源直接移动,从而减少两次深拷贝

    拷贝
    拷贝
    移动
    原始对象
    临时对象
    目标对象

    2.4 move

    move 是一个模板函数,它接受一个左值引用,并返回一个右值引用。这允许我们指示编译器,我们可以安全地“移动”这个左值的资源,而不是复制它们。

    template<class _Ty>
    inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
    {
    	// forward _Arg as movable
    	return ((typename remove_reference<_Ty>::type&&)_Arg);
    }
    

    ps:move 只是一个“建议”或“请求”,而不是强制。它告诉编译器:“这个对象我不再需要了,你可以安全地将其资源移动给另一个对象。”

    但是,如果对象的类型没有定义移动构造函数或移动赋值运算符,或者这些函数被标记为 delete,那么编译器仍然会进行复制操作。


    同时,使用move一定要慎重,如果要进行资源移动,要确保move的左值不会再使用。

    void test()
    {
    	string s1 = "hello";
    	string s2 = move(s1);
    }
    

    以上代码中,move后的s1被识别为右值,调用右值引用的移动构造,将s1的资源移动到s2,而s1本身就被置空了。

    2.5 移动插入

    C++11更新后,STL中所有容器都新增了移动版本的插入函数。那么,它与原先的插入函数有什么不同呢?


    先来看看以下代码:

    void test()
    {
    	vector<string> v;
    	v.push_back("1111");
    }
    

    C++98:void push_back (const T& val);
    先利用右值构造string,再拷贝构造插入vector。

    C++11:void push_back (T&& val);
    先利用右值构造string,再移动插入vector。

    综上比较,移动插入相较于传统插入,减少了一次深拷贝,效率得到了提高。

    ps:const& 延长右值生命周期(C++98)

    三、完美转发

    3.1 万能引用

    template<typename T>
    void PerfectForward(T&& t)//万能引用(引用折叠)
    {}
    

    函数模板参数中的T&&,不再代表右值引用,而是代表万能引用(又称引用折叠)。它能以统一的方式处理左值和右值,既能接收左值引用,也能接收右值引用

    • t为右值时,保持为T&&
    • t为左值时,折叠为T&

    3.2 forward

    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)//万能引用(引用折叠)
    {
    	Fun(t);
    }
    
    void test()
    {
    	PerfectForward(10);//右值
    	
    	int a;
    	PerfectForward(a);//左值
    	PerfectForward(move(a));//右值
    
    	const int b = 8;
    	PerfectForward(b);//const 左值
    	PerfectForward(move(b));//const 右值
    }
    

    前面已经提到,右值的引用属性为左值(只有这样设计才能实现移动语义),那么在上述代码中,调用Fun函数就全部是左值引用,无法达到区分左值和右值的效果。

    那么,如何在传递中保持参数的属性呢?这时就要用到完美转发!


    forward 是一个模板函数,如果接收左值引用,则返回左值引用,如果接收右值引用,则返回右值引用

    template<typename T>
    void PerfectForward(T&& t)//万能引用(引用折叠)
    {
    	Fun(forward<T>(t));//完美转发
    }
    

    完美转发允许函数模板将其参数以原始值类别(左值或右值)转发给另一个函数。这通常用于包装或委托函数。

    四、新增默认成员函数

    class Person
    {
    public:
    	Person(const char* name = "", int age = 0)
    		:_name(name)
    		, _age(age)
    	{}
    
    	//Person(const Person& p)
    	//	:_name(p._name)
    	//	,_age(p._age)
    	//{}
    
    	//Person& operator=(const Person& p)
    	//{
    	//	if(this != &p)
    	//	{
    	//		_name = p._name;
    	//		_age = p._age;
    	//	}
    	//	return *this;
    	//}
    
    	//~Person()
    	//{}
    private:
    	my::string _name;
    	int _age;
    };
    

    4.1 移动构造函数

    若未显式定义,且未显式定义拷贝构造、拷贝赋值、析构,编译器才会自动生成默认的移动构造函数。对内置类型值拷贝,对于自定义类型调用其移动构造函数(若未显式定义,则调用其拷贝构造)

    4.2 移动赋值重载

    若未显式定义,且未显式定义拷贝构造、拷贝赋值、析构,编译器才会自动生成默认的移动赋值重载。对内置类型值拷贝,对于自定义类型调用其移动赋值重载(若未显式定义,则调用其拷贝赋值重载)

    void test()
    {
    	Person s1;
    	Person s2 = s1;
    	Person s3 = move(s1);//移动构造
    	Person s4;
    	s4 = move(s2);//移动赋值
    }
    

    4.3 default

    强制生成默认成员函数

    Person(Person&& p) = default;//强制生成默认移动构造
    Person& operator=(Person&& p) = default;//强制生成默认移动赋值
    

    4.4 delete

    禁止生成默认成员函数

    Person(const Person& p) = delete;//禁止生成默认拷贝构造
    Person& operator=(const Person& p) = delete;//禁止生成默认拷贝赋值
    

    ps:C++98中,将函数设置为private,以此达到禁止生成默认成员函数的目的。


    真诚点赞,手有余香

  • 相关阅读:
    虚拟机--无法连接网络
    Qt正则表达式
    DRM Memory Management
    超图WMTS服务的几个关键参数
    kube-scheduler源码分析(2)-核心处理逻辑分析
    Elasticsearch Data Stream 数据流使用
    Mybatis04(关联关系映射)
    C++ 正则表达式使用
    从零搭建开发脚手架 本地事务和远程调用等操作的剥离
    03 基础张量操作_3
  • 原文地址:https://blog.csdn.net/2301_79188764/article/details/139213030