【C++】-- C++11 - 右值引用和移动语义(上万字详细配图配代码从执行一步步讲解)

news2024/12/25 12:23:00

目录

左值引用和右值引用

右值引用使用场景和意义

移动语义

传值返回问题

移动构造

移动赋值

总结

解决传值插入问题

完美转发

模板中的&&万能引用

完美转发std::forward

完美转发实际中的使用场景


左值引用和右值引用

        其实在C++11之前,C++没有左右值之分,只有一种引用。而在传统的C++语法中的引用的语法存在的情况下,C++11中又新增了的引用语法特性,就有了左值引用和右值引用之分,所以从现在开始我们之前学习的引用就叫做左值引用。

哪什么是左值引用和右值引用,在左边的一定是左值引用,在右边的一定是右值引用吗?

        答:不是的!

什么是左值?什么是左值引用?

        左值可以出现赋值符号的左边,也可以出现在赋值符号的右边。无论左值引用还是右值引用,都是给对象取别名。左值也有一个例外(const),我们可以获取它的地址,但是不能将其放在赋值符号的左边,不能被赋值修改,所以我们需要分别的看待。

        左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,但是左值引用的核心:我们可以获取它的地址,因为基本上百分之99的值,左值引用都可以对它进行赋值。但是定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值起别名。

什么是左值?

        我们可以获取它的地址的就是左值。

int main()
{
	//左值(表示数据的表达式):可以取地址 + (可以对它赋值<--不是一定的)
	int a = 10;

	const int b = 20; //b左值,但是不可以对它赋值
	//b = 10;   /* error */

	int* p = &a;
	*p = 100;    //左可以是表达式所以*p是
	return 0;
}

什么是左值引用?

        对左值取别名的就是左值引用,即:以前所学的引用都叫做左值引用。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;

	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

什么是右值?什么是右值引用?

        右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(主要指的是传值返回)(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边右值不能取地址。右值引用就是对右值的引用,右值引用就是给右值取别名

什么是右值?

        右值的特点:不能取地址。取地址会报不是左值的错误。

double fmin(double x, double y)
{
	double min = x;
	if (min > y)
		return y;
	return x;
}

int main()
{
	double x = 1.1, y = 2.2;

	// 以下几个都是常见的右值:不能取地址
	10;
	x + y;
	fmin(x, y);

	return 0;
}

        为什么 x+y 与 fmin(x, y) 会产生一个右值呢?因为 fmin(x, y) 是传值返回,会产生一个临时对象,x+y 也会产生一个临时对象,所以可以认为临时对象就是右值。

什么是右值引用?

        右值引用与左值引用的区别是:两个 ‘&’ 。右值引用是给右值取别名。

double fmin(double x, double y)
{
	double min = x;
	if (min > y)
		return y;
	return x;
}

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);

	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);

	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;

	return 0;
}

左值引用能引用右值吗?

        左值引用是不能直接的引用右值的:

int main()
{
	double x = 1.1, y = 2.2;
	double& sum = x + y; // error C2440: 无法从“double”转换为“double & ”
	return 0;
}

        右值最大的缺陷就是不能被改变的,左值引用的意义是能被改变的,于是左值引用需要加一个 const,即:const的左值引用才能引用右值

int main()
{
	double x = 1.1, y = 2.2;
	const double& sum = x + y;
	return 0;
}

        利用左值引用引用右值的意义:正如我们前面的引用所学。当函数传参接受的时候:

template<class T>
int FunC(T x){}

        x 与 y 都可以通过拷贝,接收左值或右值的数据,但是拷贝毕竟是花费额外的空间,如果T为自定义类型的时候,过大,无非是浪费空间,于是变有了以下操作:

template<class T>
int FunC(T& x){}

        然而,在进行此操作的时候有一条建议:当x值不需要被修改的时候,甚至不能被修改的时候,建议设置为const修饰:

template<class T>
int FunC(const T& x){}

        这是对不能被修改的数据的保护,也是让其在能接收左值的同时也能接收右值。当然了,当需要被修改的时候也就不能用const修饰了,也就不能传右值了。

右值引用能引用左值吗?

        右值引用是不能直接的引用左值的。

int main()
{
	int a = 10;
	int&& r2 = a;	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	return 0;
}
        右值引用可以move以后的左值。
#include<utility>
using namespace std;

int main()
{
	int a = 10;

	// 右值引用可以引用move以后的左值
	int&& r3 = move(a);
	return 0;
}
左值引用总结:
  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
右值引用总结:
  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。

        需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,编译器会为其开辟一个空间,然后将10存进去,再引用。也就是说,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,实际中右值引用的使用场景并不在于此,这个特性也不重要。(作为了解)

#include<iostream>
using namespace std;

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;

	rr1 = 20;
	rr2 = 5.5;  // 报错

	cout << &rr1 << endl;
	cout << &rr2 << endl;

	return 0;
}

        右值被右值引用之后会变成左值。

右值引用使用场景和意义

问:先不说左值引用与右值引用,就说引用是用来干什么的?

        引用的价值:减少拷贝(拷贝的代价很大,尤其是深拷贝)

        本来就在C++11之前的应用就可以引用左值也可以引用右值,就是加const罢了,没有任何的问题。所以引用的核心价值就是在减少拷贝。自C++11之后区分的就更加详细了些:

  •  左值引用解决的问题:
    • 做参数:   a. 减少拷贝,提高效率     b. 做输出性参数将修改值带回,如:swap函数
    • 做返回值:  a减少拷贝,提高效率   b. 引用返回,可以修改返回对象,如:operator[] 

        看起来左值引用解决的很完美,其实左值引用只是解决了绝大多数的问题。做返回值的问题其实解决的不行。 只解决了百分之70的问题。

        比如;将数值转为string的函数,左值引用是解决不利了的,左值引用不敢用。

        to_string是经典的传值返回,因为如果我们简单的实现to_string,会发现:

#include<string>
using namespace std;
namespace cr
{
	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		string str;   // str在这个地方是一个局部对象 -- 出作用域就销毁了,根本不敢用左值引用
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}

		reverse(str.begin(), str.end());
		return str;
	}
}

        str在to_string函数中是一个局部对象,是出了作用域就会销毁,所以根本不敢用左值引用。如果用左值引用这里就崩了,因为返回了它的别名,但是其其实已经析构了。

string& to_string(int value)  // 左值引用程序会崩溃
{
    //……
	string str;   // str在这个地方是一个局部对象 -- 出作用域就销毁了,根本不敢用左值引用
    //……
	return str;
}

        对于这种类似的情况,有一道题也是同样的:
118. 杨辉三角https://leetcode.cn/problems/pascals-triangle/description/

        如果是vector<int>也就算了,还能接受,但是其是vector<vector<int>>这样的类型,其数据量将会是恐怖的,但是又因为是内部创建的局部变量,出了作用域就调用析构函数,所以根本不敢使用左值引用。

        C++98的左值引用处理上面的场景是很难的。

用C++98处理只会有:

  • 使用全局处理:

        是不好的,会有安全问题,线程安全问题。在多线程同时调用这个函数的时候,是会为了此资源发生竞争的问题。(会有多线程的程序,谨慎使用静态变量与全局变量)

  • 使用new:

        会有内存泄漏的问题new出来的空间,是需要使用delete的,万一忘记了。并且,有时候会在想到释放的时候抛异常,就会有内存泄漏的问题。

  • 使用输出型参数:

        这是C++98最好的处理方式。也是公司所会采取的方式。

         但是此方法也是有不好的地方,其是不太符合使用习惯的。

移动语义

传值返回问题

C++11右值引用一个重要的功能就是解决上面的情况:

        右值引用的使用方法是与左值应用的使用方法截然不同的。下面研究右值引用是怎么使用的,并且是如何做到的:
(为方便讲解,此处使用一个模拟实现的string容器,未添加移动构造与移动赋值)

#include<string>
#include<iostream>
#include<assert.h>
using namespace std;

namespace cr
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::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;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		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;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		reverse(str.begin(), str.end());
		return str;
	}
}

int main()
{
	cr::string ret = cr::to_string(1234);

	return 0;
}

cr::string ret = cr::to_string(1234); 调用了几次拷贝构造?

        调用了2次拷贝构造,编译器优化成了一次拷贝构造。

Note:

        也有的编译器是一次拷贝构造都没有进行,比如Linux的g++,因为g++一看你虽然搞出来了一个ret的值,但是你并没有去使用它,所以直接优化的一次也不给你拷贝了。这种时候就需要使用ret一下才会有一次拷贝构造。因为Linux的g++的默认情况是Release版本。优化会更凶。

        可以利用Linux,通过参数-fno-elide-constructors关闭g++的编译优化,打印未优化时调用的结果:

        而为了压入调用to_string函数的返回值,而在main函数开辟的空间,是在该语句结束后销毁,即在拷贝给ret后销毁。

        优化成调用一次拷贝构造,其实就是相当于它把中间生成的值给干掉了,就是编译器觉得很多余,既然是给ret的,那直接给以str给ret就行了,直接一步到位

问:既然这个样子,那为什么编译器要设置一个临时栈帧存这个临时变量?不要岂不是更好?

        答:不可以!

        因为在有一些场景下,如定义后,其在一些列的操作下再使用+=,就是不能被省略优化的了(这个地方需要移动赋值解决)

int main()
{
	cr::string ret;
	 // ……  一系列的操作
	ret = cr::to_string(1234);
	return 0;
}

        因为,ret定义与to_string并不是在一个地方的,只有在一条语句下才能优化


        此处,最后多一个拷贝构造是因为,string容器的模拟实现的operator=的实现方法:所以,将其看作一次拷贝构造(深拷贝),一次拷贝赋值(深拷贝)即可。因为这的拷贝赋值由一次拷贝构造实现。

问:那这些地方怎么办呢?右值引用又是如何起的作用?

        右值引用不是直接起作用而是间接起作用。右值引用在这个地方增加了两个函数。

原本在C++98是拥有,拷贝构造与拷贝赋值。

// 拷贝构造
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;
}

        C++98是通过const string& s可以引用右值的特点执行。

C++11又提供增加了,移动构造与移动赋值。

移动构造

        移动构造与拷贝构造的区别:

// 拷贝构造
string(const string& s)
	:_str(nullptr)
{
	cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

	string tmp(s._str);
	swap(tmp);
}

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动资源" << endl;
	swap(s);
}

        其利用了,编译器会按照,最符合该路径的路径执行,虽然拷贝构造可以执行右值,但是移动构造更加符合右值,于是右值都执行移动构造。

(只添加了移动构造的string模拟容器)

#include<string>
#include<iostream>
#include<assert.h>
using namespace std;

namespace cr
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动资源" << endl;
			swap(s);
		}

		// 拷贝赋值
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		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;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		reverse(str.begin(), str.end());
		return str;
	}

}

C++11增加了对右值的定义同时,将其划分为:

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

        之所以叫将亡值,是因为如字面常量、表达式返回值,函数返回值返回,其在执行完它的那条语句之后,就会销毁,就死亡了。而移动构造就是抓住自定义类型右值死亡销毁之前,与其做一条交易,将数据给我,你都将死亡了,带着这些数据也没有用,也就是带去销毁,给我吧。

int main()
{
	cr::string str1("hello");
	cr::string str2(str1);

	cr::string str3(move(str1)); // move之后的左值变为右值
	return 0;
}

        之所以其的数据_str = nullptr,size = 0, capacity = 0。是因为移动构造的定义:

移动构造的价值:
        在拥有移动构造后的:

int main()
{
    cr::string ret = cr::to_string(1234);

    return 0;
}

        调用的不是一次拷贝钩构造了,而是一次移动构造。

此处,只进行一次移动构造的原理:

未优化的情况:

        虽然str是左值,但是编译器发现执行完return str; 就函数结束了,str销毁了,于是将其看作了右值, 于是执行移动构造。

 优化后的情况:

          优化成调用一次移动构造,其实就是相当于它把中间生成的值给干掉了,就是编译器觉得很多余,既然是给ret的,那直接给以str给ret就行了,直接一步到位

 ·       拷贝构造的代价 > 移动构造的代价。因为拷贝构造拷贝了还要将旧的资源进行释放。是一次深拷贝加一次前面的值的释放。而移动构造是将资源转移过来,不需要拷贝。

        右值引用此处最重要的功能:透过移动构造,在传值的场景下来减少拷贝

移动赋值

此处是移动构造与移动赋值都为存在:

        一方面:由于移动构造会在未有移动赋值时起作用,而此处本应移动构造起作用的点。所以此处忽略掉移动构造

        二方面:移动构造与移动赋值本就是在C++11同时出现,为的是解决C++98的问题。所以只有在忽略掉移动构造才能凸显移动构造解决的C++98问题。

int main()
{
	cr::string ret;
	// ……一系列操作
	ret = cr::to_string(1234);
	return 0;
}

        这个点也是在移动构造篇中讲解到的C++98无法优化的场景。

        此处,最后多一个拷贝构造是因为,string容器的模拟实现的operator=的实现方法:所以,将其看作一次拷贝构造(深拷贝),一次拷贝赋值(深拷贝)即可。因为这的拷贝赋值由一次拷贝构造实现。

        移动赋值与拷贝赋值的区别:

// 拷贝赋值
string& operator=(const string& s)
{
	cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
	string tmp(s);
	swap(tmp);

	return *this;
}

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动赋值(资源移动)" << endl;
	swap(s);

	return *this;
}

 而在同时加入移动构造与移动赋值之后:

(同时添加了移动构造与移动赋值的string模拟容器)

#include<string>
#include<iostream>
#include<assert.h>
using namespace std;

namespace cr
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

			string tmp(s._str);
			swap(tmp);
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动资源" << endl;
			swap(s);
		}

		// 拷贝赋值
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值(资源移动)" << endl;
			swap(s);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		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;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		reverse(str.begin(), str.end());
		return str;
	}
}
int main()
{
	cr::string ret;
	// ……一系列操作
	ret = cr::to_string(1234);
	return 0;
}

        在将将亡值str会带走得资源换下来并保留继续使用的同时,并将ret原有的无用资源交给将亡值str让其带走。

C++11将STL库也进行了更新,此处举例几个容器:

string容器的构造函数相关文档

string容器的operator=相关文档

vector容器的构造函数相关文档

vector容器的operator=相关文档

总结

        移动构造与移动赋值在拷贝构造与拷贝赋值的角度上大大的节省了空间,也大大的提升了效率。

        对于函数的传值返回,右值引用不是直接起作用的,与左值引用不同。右值引用是通过移动构造与移动赋值起作用的。通过函数的传值返回是一个右值(将亡值),然后通过转移它的资源来减少拷贝。

        拷贝构造与拷贝赋值的主要数据是通过不断的复制拷贝传递,移动构造与移动赋值的主要数据是通过不断的击鼓传花传递。

        有一些地方说右值引用延长了数据的声明周期,这是不完全准确的,从上面的讲解可以看到,右值引用是通过不断的转移资源,来确保了数据的保留。并未干扰到析构的时机。

解决传值插入问题

C++11将STL库中容器的插入也进行了更新,此处举例几个容器:

list容器的push_back相关文档

vector容器的pusk_back相关文档

int main()
{
	cr::string s1("hello");

	cout << "--------------- vector -------------------" << endl;
	vector<cr::string> v;
	v.push_back(s1);

	cout << "===================================" << endl;

	v.push_back(cr::string("world"));


	cout << "----------------- list -------------------" << endl;
	list<cr::string> lt;
	lt.push_back(s1);

	cout << "===================================" << endl;

	lt.push_back(cr::string("world"));

	return 0;
}

        vector容器与list容器的push_back右值引用版,有些许的不同是因为其内部,源代码的不同所导致的。但是其本质上都是有一次资源移动(移动构造)。

以list容器的push_back做分析:

int main()
{
	list<cr::string> v;
	cr::string s1("1111");
	v.push_back(s1);

	v.push_back("2222");
	v.push_back(std::move(s1));
	return 0;
}

        左值:把对象构造上去。由于左值不能也不敢转移资源,移动资源。所以左值构造就是拷贝构造。
        右值:把对象构造上去。由于右值是将亡值,所以没有必要调用拷贝构造,需要调用移动构造。

所以,对于插入接口都提供了右值引用接口:

        STL容器,插入接口C++11后都提供右值版本。插入过程中,如果传递对象是右值对象,那么进行资源转移减少拷贝。

vector容器的相关文档

list容器的相关文档

完美转发

模板中的&&万能引用

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

        如果是普通参数的这个版本叫做右值版本,但是如果模板之后提供这个版本,可以叫做万能引用,也可以叫做引用折叠。

         万能引用:其既能引用左值也能引用右值。
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; }

// 万能引用:T既能引用左值,也能引用右值
// 引用折叠:a.左值&&会被折叠为&;b.右值会被右值引用然后变为左值,折叠为左值
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main()
{
	PerfectForward(10);				// 右值
	int a;
	PerfectForward(a);				// 左值
	PerfectForward(std::move(a));   // 右值
	const int b = 8;
	PerfectForward(b);				// const 左值
	PerfectForward(std::move(b));   // const 右值
	return 0;
}

        左值引用后是左值。右值引用后变为左值。通通变为左值
        右值经过右值引用变为左值,正是前面所提的了解的知识点需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,编译器会为其开辟一个空间,然后将10存进去,再引用。也就是说,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,实际中右值引用的使用场景并不在于此,这个特性也不重要。(作为了解)

        可以理解为,编译器是为了实现一些底层所导致的。至于那些底层,过复杂,没有必要了解研究。

问:那我们这么保持属性呢?

        答:利用完美转发std::forward。

完美转发std::forward

        std::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)
{
	// 完美转发:保持t引用对象属性
	Fun(std::forward<T>(t));
}

int main()
{
	PerfectForward(10);				// 右值
	int a;
	PerfectForward(a);				// 左值
	PerfectForward(std::move(a));   // 右值
	const int b = 8;
	PerfectForward(b);				// const 左值
	PerfectForward(std::move(b));   // const 右值
	return 0;
}

         是左值就是左值,是右值就是右值。不再会折叠掉属性。

完美转发实际中的使用场景

(利用list容器的部分模拟实现以及string容器的部分模拟实现讲解)

当未使用完美转发的时候:

namespace cr
{
	class string
	{
	public:
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;

			_str = new char[s._capacity + 1];
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		// 移动构造
		string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 资源转移" << endl;
			swap(s);
		}

		// 拷贝赋值
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
			swap(s);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

namespace cr
{
	// List容器节点
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next = nullptr;
		ListNode<T>* _prev = nullptr;
		T _data;

		ListNode(const T& x = T())
			:_data(x)
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};

	// List容器
	template<class T>
	class List
	{
		typedef ListNode<T> Node;
	public:
		List()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}

		// 当不使用完美转发的时候
		void PushBack(T&& x)
		{
			Insert(_head, x);
		}

		void PushBack(const T& x)
		{
			Insert(_head, x);
		}

		// 当不使用完美转发的时候
		void Insert(Node* pos, T&& x)
		{
			Node* prev = pos->_prev;
			Node* newnode = new Node(x);

			// prev newnode pos的连接
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}

		void Insert(Node* pos, const T& x)
		{
			Node* prev = pos->_prev;
			Node* newnode = new Node(x);

			// prev newnode pos的连接
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = pos;
			pos->_prev = newnode;
		}
	private:
		Node* _head;
	};
}

int main()
{
	cr::List<cr::string> lt;

	cout << "----------------------------------" << endl;

	lt.PushBack("world");
	return 0;
}

        由于,引用折叠,左值&&会被折叠为&,右值会被右值引用然后变为左值。所以根本下一步不会右值,左值与右值在前一步皆退化了,变为左值。

当使用完美转发的时候:

代码改变的部分:

// 当使用完美转发的时候
void PushBack(T&& x)
{
	Insert(_head, std::forward<T>(x));
}

// 当使用完美转发的时候
void Insert(Node* pos, T&& x)
{
	Node* prev = pos->_prev;
	Node* newnode = new Node(std::forward<T>(x));

	// prev newnode pos的连接
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = pos;
	pos->_prev = newnode;
}

增加后的运行结果: 

增加这么多是因为其是一个 “环” :
第一步:

第二步:

第三步:

note:

        需要使用完美转发std::forward,否者因为:引用折叠,左值&&会被折叠为&,右值会被引用然后变为左值。最终还是调用一次拷贝构造。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/133123.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

系统管理员喜欢 systemd 的 5 个理由

导读systemd 的速度和易用性使其成为管理现代 Linux 系统的流行方式。 系统管理员知道&#xff0c;在一台运行着的现代计算机上会发生很多事情&#xff1a;应用程序在后台运行、预定事件等待在特定时间被触发、事件写入日志文件、发送状态报告。在以前&#xff0c;不同的进程可…

为什么深度神经网络这么难训练

目录 1、深度网络训练困难的原因&#xff1a; 2、消失的梯度问题 3、不稳定的梯度问题 参考文章&#xff1a;为什么很难训练深度神经网络&#xff1f; - 腾讯云开发者社区-腾讯云 1、深度网络训练困难的原因&#xff1a; 训练速度慢 在深度网络中&#xff0c;不同的层学习…

《UEFI内核导读》UEFI Firmware Storage简介

敬请关注&#xff1a;“固件C字营 UEFI固件一般存储在被称之为“固件仓库”的非易失性存储器中&#xff0c;简称为FD&#xff08;固件设备&#xff09;&#xff0c;当前主流的存储介质是NorFlash它拥有非易失性、XIP以及可二次编程的特性。 固件设备可以分为物理设备和逻辑设备…

linux如何发送查收邮件的详解

一&#xff0c;linux用户发送给linux中的其它用户 1&#xff0c;使用命令 yum install sendmail -y安装sendmail软件 2&#xff0c;使用yum install mailx -y安装 mailx软件 3&#xff0c;使用命令systemctl start sendmail启动sendmail&#xff08;此过程消耗时间&#xff09…

你在项目里遇到的最大困难是什么,如何解决的?

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/Tyson0314/Java-…

Weiler-Atherton 算法介绍(简单易懂)

目录 一、算法介绍 二、算法描述 三、算法总结 一、算法介绍 Weiler Atherton多边形裁剪算法是一种允许裁剪凹面算法的算法。与 Sutherland-Hodgman 多边形裁剪算法最主要的区别是&#xff0c;该算法能够裁剪凹多边形&#xff0c;并不留下任何残留物。 裁剪示意图&#xff1…

关于微服务,这些你都了解吗-微服务介绍

文章目录一 认识微服务1.1 什么是微服务1.2 微服务的特点1.3 微服务诞生背景1.4 微服务架构的优势二 微服务生态1.1 硬件层1.2 通信层1.3 应用平台层1.4 微服务层三 微服务详解1.1 微服务架构1.2 服务注册和发现1.3 rpc调用和服务监控四 微服务与DDD1.1 什么是DDD1.2 DDD作用1.…

python快速实现某东方视频解密wasm算法

开始之前请大家先去了解一下 wasm这种技术(可以百度搜索一下 WebAssembly是什么?) 现在开始.... 1&#xff0c;先看一张图 首先写一个本地加载wasm的方法 00043706.wasm就是当前网站load的wasm库,如果遇到报错,请联系我&#xff0c;文章最后有qq联系方式 let u {} functio…

C++类和对象2:默认成员函数

我们通过this指针可以看出来&#xff0c;C其实隐藏了非常多的东西&#xff0c;很多事情它会在编译的时候包揽&#xff0c;那么作为最为重要的类和对象&#xff0c;它是不是还隐含了更多我们平常看不到的东西呢&#xff1f; 我们创建一个空类里面啥也不放。 class Text{}; 看上…

2_类加载子系统

目录 概述 类加载器子系统作用 类的加载过程 加载阶段 加载class文件的方式 链接阶段 准备 Prepare 解析 Resolve 初始化阶段 类加载器的分类 虚拟机自带的加载器 扩展类加载器&#xff08;Extension ClassLoader&#xff09; 应用程序类加载器&#xff08;系统类加…

阿里云计算工程师ACP考题归类解析

目录考纲答题技巧四式记的牢三妙招一、对象存储OSS二、专有网络VPC三、服务器ECS四、安全五、阿里云弹性伸缩Auto Scaling五、内容分发网络CDN总结考纲 重点学习ECS、VPC、OSS三部分。 答题技巧四式 战略是先做简单后做难度高的。 一、简化 做题问三个问题&#xff0c;按回答…

【云原生 | Kubernetes 实战】20、K8s Ingress 实现业务灰度发布

目录 通过 Ingress-nginx 实现灰度发布 一、Ingress Controller 多种发布策略介绍 场景一&#xff1a;将新版本灰度给部分用户 场景二&#xff1a;切一定比例的流量给新版本 二、模拟部署生产测试版本 Web 服务 2.1 部署一个 v1 版本: 2.2 再部署一个 v2 版本: 2.3 再…

论文投稿指南——中文核心期刊推荐(化学)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

本地方法接口

什么是本地方法 简单地讲&#xff0c;一个Native Methodt是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法&#xff1a;该方法的实现由非Java语言实现&#xff0c;比如C。这个特征并非Java所特有&#xff0c;很多其它的编程语言都有这一机制&#xff0c;…

AQS核心原理之

AQS系列 1、AQS核心原理之 2、ReentrantLock 原理及示例 文章目录AQS系列一、什么是AQS?二、AQS特性三、AQS内部维护 state四、队列4.1 同步等待队列4.2 条件等待队列5、总结一、什么是AQS? AQS全称是 AbstractQueuedSynchronizer&#xff08;抽象对了同步器&#xff09;&am…

Cento6从零开始用Nginx+mysql+php搭建Discuz在线论坛系统

首先我的liunx使用版本是 centos6.5 32位 discuz版本是Discuz_X3.2_SC_UTF8.zip Xshell版本是6 xftp是7版本 mysql也是是这个版本&#xff1a;mysql这里写目录标题1.yum安装php2.安装nginx包3.安装php-fpm4.安装php-mysql5. 安装Discuz在线论坛系统由于目前yum源已经无法使用需…

力扣 2037. 使每位学生都有座位的最少移动次数

题目 一个房间里有 n 个座位和 n 名学生&#xff0c;房间用一个数轴表示。给你一个长度为 n 的数组 seats &#xff0c;其中 seats[i] 是第 i 个座位的位置。同时给你一个长度为 n 的数组 students &#xff0c;其中 students[j] 是第 j 位学生的位置。 你可以执行以下操作任…

unidbg案例-爱库存app之sig和sign分析

新年的第一篇文章,新的一年继续加油,奥利给!冲冲冲。 今天分析的app是爱库存,版本号6.1.6,这次还是使用unidbg分析该样本,加密参数有很多,不过只关注sig和sign两个参数。 老规矩,上来先抓个包。 1.抓包 可以看到上面👆🏻的sign,就是本次研究的重点。 2.jadx静…

A* 算法详解(超级详细讲解,附有大图)

目录 引入 一.基本概念 二.算法原理 ①用宽度优先搜索 ②狄克斯特拉算法 ③A*算法 三.需要注意 四.c伪代码 最后 引入 今天想跟大家聊的&#xff0c;是我们经常用到&#xff0c;但是却让大家觉得十分神秘的那个算法&#xff1a;A* 。 这是一个远古而又非常经典的游戏…

【C++ STL】-- 用一棵红黑树的插入实现同时封装map与set

用一棵红黑树同时封装map与set的意义&#xff1a;所谓的 “用一棵红黑树同时封装map与set” 只是在程序员的角度&#xff0c;通过一系列手段&#xff0c;以一个红黑树同时满足map与set。但是在编译器的角度&#xff0c;实际上并不是一颗树实现的&#xff0c;程序员所写的只是一…