【C++】C++11-右值引用和移动语义

目录

1、左值与右值

2、左值引用与右值引用

3、左值引用和右值引用的底层

4、左值引用的不足

5、移动构造

6、移动赋值运算符重载

7、插入操作

8、完美转发


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

1、左值与右值

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边,左值可以出现在=的右边。定义时const修饰符后的左值,不能给他赋值(也就是不能出现在=左边,如下面的c),但是可以取它的地址。

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址右值大多数时候是一些常量、临时对象、匿名对象

判断左值和右值,最好的判断条件就是看其是否能取地址

int main()
{
	// 左值:可以取地址
	// p,b,c,d,*p,s,s[0]都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	const int d = b;// 左值是可以出现在右边的
	*p = 10;
	string s("7777777");
	s[0]; // operator[]返回的是里面的左值的引用
	
	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值,通常为常量、临时对象、匿名对象
	10;
	x + y;
	fmin(x, y);
	string("7777777");
	return 0;
}

fmin是返回两者之中较小的那一个

左值和右值不一定是值,也可以是表达式,像这里面*p就是表达式

2、左值引用与右值引用

左值引用就是给左值取别名,右值引用就是给右值取别名

int main()
{
	// 左值:可以取地址
	// p,b,c,d,*p,s,s[0]都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	const int d = b;// 左值是可以出现在右边的
	*p = 10;
	string s("7777777");
	s[0]; // operator[]返回的是里面的引用
	
	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值,通常为常量、临时对象、匿名对象
	10;
	x + y;
	fmin(x, y);
	string("7777777");

	// 左值引用:给左值取别名
	int*& r1 = p;
	int& r2 = b;
	int& r3 = *p;
	// 右值引用:给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x , y);
	return 0;
}

上面是左值引用给左值取别名,右值引用给右值取别名

实际上,左值引用可以给右值取别名,右值引用也可以给左值取别名

// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值,通常为常量、临时对象、匿名对象
10;
x + y;
fmin(x, y);
string("7777777");

// 左值引用给右值取别名:不能直接引用,需要加const
//int& rx1 = 10; // 是错的
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("7777777");

这在我们之前设计STL的时候有很多应用,如vector的push_back就是用的const 左值引用,void push_back(const T& x)

// 左值:可以取地址
// p,b,c,d,*p,s,s[0]都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
const int d = b;// 左值是可以出现在右边的
*p = 10;
string s("7777777");
s[0]; // operator[]返回的是里面的引用

// 右值引用给左值取别名:不能直接引用,需要move(左值),先将左值移动为右值
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);

move是传入左值,返回右值,传入的左值是不会变的

3、左值引用和右值引用的底层

int main()
{
	int x = 0;
	int& r1 = x;
	int&& rr1 = x + 10;
	return 0;
}

现在我们将上面这段代码转到反汇编,观察以下底层是如何实现的

这里面的rbp+64h是地址,可以将显示符号名取消

ebp是高地址,栈是向下生长的

所以,无论是左值引用还是右值引用,到了汇编层,都是指针(左值引用、右值引用、类型等只是编译器检查这一层的概念而已,到了汇编层就都没有了。左值引用的指针直接指向对象的空间,右值引用会先开辟一块空间,再指向开辟的空间。move的本质就是强制类型转换,只是实现较为复杂。所以,右值也是有地址的。底层与语法有时候是背离的

所以,右值开了空间,并且有地址,取不了是因为编译器的限制,具有常性也是因为编译器的限制

int main()
{
	int x = 0;
	int& r1 = x;
	// 因为底层没有引用,所以下面这样只是语法上不允许而已,只需对其强制类型转换即可
	//int&& rr1 = x;
	int&& rr1 = (int&&)x;
    int&& rr2 = move(x);
	return 0;
}

4、左值引用的不足

我们引入引用的意义就是为了减少拷贝

左值引用解决的场景:引用传参、引用传返回值

左值引用没有彻底解决的场景:引用传返回值(当返回值出了函数作用域就没有时,不能用左值引用传返回值)

为了能有一个真实场景,这里使用之前自己实现的string,并且为了观察,这个string不使用现代写法

string.h

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace cxf
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		//默认成员函数
		string(const char* str = "");
		~string();
		string(const string& s);
		string& operator=(const string& s);//传统写法的赋值运算符重载
		//string& operator=(string s);//现代写法的赋值运算符重载

		//迭代器
		iterator begin();
		iterator end();
		const_iterator begin() const;
		const_iterator end() const;

		//插入删除函数
		void reserve(size_t n);
		void resize(size_t n, char ch = '\0');
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);
		string& insert(size_t pos, char ch);
		string& insert(size_t pos, const char* str);
		string& erase(size_t pos, size_t len = npos);

		//relational operators
		bool operator<(const string& s) const;
		bool operator==(const string& s) const;
		bool operator<=(const string& s) const;
		bool operator>(const string& s) const;
		bool operator>=(const string& s) const;
		bool operator!=(const string& s) const;

		//其他函数
		void swap(string& s);
		const char* c_str() const;
		int size() const;
		int capacity() const;
		char& operator[](size_t i);
		const char& operator[](size_t i) const;
		size_t find(char ch, size_t pos = 0) const;
		size_t find(const char* str, size_t pos = 0) const;
		void clear();
	private:
		char* _str = nullptr;
		int _size;
		int _capacity;
		const static size_t npos;
	};
	//输入输出函数
	std::ostream& operator<<(std::ostream& out, const string& s);
	std::istream& operator>>(std::istream& in, string& s);
	std::istream& getline(std::istream& in, string& s);
	// 将整型转为string
	string to_string(int value);
}

string.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"string.h"
namespace cxf
{
	const size_t string::npos = -1;
	void string::swap(string& s)
	{
		std::swap(_str, s._str);
		std::swap(_size, s._size);
		std::swap(_capacity, s._capacity);
	}
	string::string(const char* str)
		:_size(strlen(str))
	{
		_str = new char[_size + 1];
		strcpy(_str, str);
		_capacity = _size;
	}
	string::~string()
	{
		delete[] _str;
		_str = nullptr;
		_size = _capacity = 0;
	}
	//传统写法的拷贝构造
	string::string(const string& s)
	{
		cout << "拷贝构造" << endl;
		_size = s._size;
		_capacity = s._capacity;
		_str = new char[_capacity + 1];
		strcpy(_str, s._str);
	}
	//现代写法的拷贝构造
	//string::string(const string& s)
	//	:_str(nullptr)// 若_str为随机值析构时可能会报错
	//{
	//	string tmp(s._str);
	//	swap(tmp);
	//}
	//传统写法的赋值运算符重载
	string& string::operator=(const string& s)
	{
		cout << "赋值运算符重载" << endl;
		delete[] _str;
		_size = s._size;
		_capacity = s._capacity;
		_str = new char[_capacity + 1];
		strcpy(_str, s._str);
		return *this;
	}
	//现代写法的赋值运算符重载
	/*string& string::operator=(string s)
	{
		swap(s);
		return *this;
	}*/
	const char* string::c_str() const
	{
		return _str;
	}
	string::iterator string::begin()
	{
		return _str;
	}
	string::iterator string::end()
	{
		return _str + _size;
	}
	string::const_iterator string::begin() const
	{
		return _str;
	}
	string::const_iterator string::end() const
	{
		return _str + _size;
	}
	int string::size() const
	{
		return _size;
	}
	int string::capacity() const
	{
		return _capacity;
	}
	char& string::operator[](size_t i)
	{
		assert(i < _size);
		return _str[i];
	}
	const char& string::operator[](size_t i) const
	{
		assert(i < _size);
		return _str[i];
	}
	void string::reserve(size_t n)
	{
		assert(n > _capacity);
		char* tmp = new char[n + 1];
		strcpy(tmp, _str);
		_capacity = n;
		delete[] _str;
		_str = tmp;
	}
	void string::resize(size_t n, char ch)
	{
		if (n < _size)
		{
			_str[n] = '\0';
		}
		else
		{
			if (n > _capacity)
			{
				reserve(n);
			}
			for (int i = _size; i < n; i++)
			{
				_str[i] = ch;
			}
			_str[n] = '\0';
		}
		_size = n;
	}
	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newcapacity);
		}
		_str[_size] = ch;
		_size++;
		_str[_size] = '\0';
	}
	void string::append(const char* str)
	{
		int len = strlen(str);
		if (_capacity < _size + len)
		{
			reserve(_size + len + 1);
		}
		strcpy(_str + _size, str);
		_size += len;
		_capacity = _size;
	}
	string& string::operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	string& string::operator+=(const char* str)
	{
		append(str);
		return *this;
	}
	string& string::insert(size_t pos, char ch)
	{
		assert(pos <= _size);
		if (_size == _capacity)
		{
			int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
			reserve(newcapacity);
		}
		int i = _size;
		while (i >= (int)pos)
		{
			_str[i + 1] = _str[i];
			i--;
		}
		_str[pos] = ch;
		_size++;
		return *this;
	}
	string& string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);
		int len = strlen(str);
		if (_capacity < _size + len)
		{
			reserve(_size + len + 1);
		}
		int i = _size;
		while (i >= (int)pos)
		{
			_str[i + len] = _str[i];
			i--;
		}
		strncpy(_str + pos, str, len);
		_size += len;
		return *this;
	}
	string& string::erase(size_t pos, size_t len)
	{
		assert(pos <= _size);
		if (len >= _size - pos)
		{
			_str[pos] = '\0';
		}
		else
		{
			int i = pos + len;
			while (i <= _size)
			{
				_str[i - pos] = _str[i];
				i++;
			}
		}
		_size -= len;
		return *this;
	}
	size_t string::find(char ch, size_t pos) const
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; i++)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}
	size_t string::find(const char* str, size_t pos) const
	{
		char* p = strstr(_str, str);
		if (p == nullptr) return npos;
		else return p - _str;
	}
	bool string::operator<(const string& s) const
	{
		int ret = strcmp(_str, s._str);
		return ret < 0;
	}
	bool string::operator==(const string& s) const
	{
		int ret = strcmp(_str, s._str);
		return ret == 0;
	}
	bool string::operator<=(const string& s) const
	{
		return *this < s || *this == s;
	}
	bool string::operator>(const string& s) const
	{
		return !(*this <= s);
	}
	bool string::operator>=(const string& s) const
	{
		return !(*this < s);
	}
	bool string::operator!=(const string& s) const
	{
		return !(*this == s);
	}
	void string::clear()
	{
		_str[0] = '\0';
		_size = 0;
	}
	std::ostream& operator<<(std::ostream& out, const string& s)
	{
		for (size_t i = 0; i < s.size(); i++)
			out << s[i];
		return out;
	}
	//std::istream& operator>>(std::istream& in, string& s)
	//{
	//  s.clear();
	//	while (1)
	//	{
	//		char ch;
	//		ch = in.get();
	//		if (ch == ' ' || ch == '\n')
	//			break;
	//		s += ch;//这样子容易造成频繁的扩容
	//	}
	//}
	std::istream& operator>>(std::istream& in, string& s)
	{
		s.clear();//需要先将string原先的内容清空
		char buff[128];
		int i = 0;
		while (1)
		{
			char ch;
			ch = in.get();
			if (ch == ' ' || ch == '\n')
				break;
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				i = 0;
				s += buff;
			}
		}
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
	std::istream& getline(std::istream& in, string& s)
	{
		s.clear();
		char buff[128];
		int i = 0;
		while (1)
		{
			char ch;
			ch = in.get();
			if (ch == '\n')
				break;
			buff[i++] = ch;
			if (i == 127)
			{
				buff[i] = '\0';
				i = 0;
				s += buff;
			}
		}
		if (i != 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}
	cxf::string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		cxf::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str += ('0' + x);
		}
		if (flag == false)
		{
			str += '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}

假设我们现在要完成下面的操作

int main()
{
	cxf::string s1 = cxf::to_string(1234);
	return 0;
}

to_string是传值返回(因为返回的str出了作用域就没了,所以不能使用传左值引用返回)。按照语法,应该是用str拷贝构造一个临时对象,然后根据这个临时对象拷贝构造s1。但是通常会被编译器简化为直接使用str去拷贝构造s1。这样,从2次深拷贝减少到了1次深拷贝。为了再减少拷贝,这时候就可以使用右值引用了

5、移动构造

//传统写法的拷贝构造
string::string(const string& s)
{
	cout << "拷贝构造" << endl;
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
}

我们之前写的拷贝构造的参数是const string& s,此时无论是左值调用拷贝构造,还是右值调用拷贝构造,都会调用到这个拷贝构造函数,进行深拷贝。像前面用左值str去拷贝构造临时对象,再用临时对象去拷贝构造s1,都会调用到这个函数。此时可以使用右值引用去写一个移动拷贝

//移动构造
string::string(string&& s)
{
	swap(s);
}

拷贝构造和移动构造构成函数重载,当左值去拷贝构造另一个值时,调用的是拷贝构造,当右值去拷贝构造另一个值时,调用的时移动构造。以我们上面那个例子为例,当有了移动构造,按照语法,会先使用左值str(str是左值,因为能够取地址)去拷贝构造出一个临时对象,再使用这个临时对象去移动构造出s1(这里移动构造就是交换临时对象空间中的内容和s1空间中的内容,交换完后临时对象析构时就顺便释放了s1原先的空间,没有深拷贝就让s1的内容变成了临时对象的内容,本质就是将右值引用的内容转移了),此时只有1次深拷贝。这个过程通常会被编译器优化为直接使用str去调用移动构造s1,这里面有两步关键点,第一,不生成之间的临时变量,第二,把str隐式move为右值。

前面所说的编译器优化指的是vs2019,在vs2022中,优化很大,是直接将s1当成是str,类似于str是s1的左值引用

6、移动赋值运算符重载

//传统写法的赋值运算符重载
string& string::operator=(const string& s)
{
	cout << "赋值运算符重载" << endl;
	delete[] _str;
	_size = s._size;
	_capacity = s._capacity;
	_str = new char[_capacity + 1];
	strcpy(_str, s._str);
	return *this;
}
// 移动赋值
string& string::operator=(string&& s)
{
	swap(s);
	return *this;
}

可以应用在下面这个场景中,同样是为了减少拷贝

int main()
{
	cxf::string s1;
	s1 = cxf::to_string(1234);
	cout << s1 << endl;
	return 0;
}

移动赋值也是类似的,编译器优化后直接使用str去调用移动赋值给s1赋值

像我们之前写的杨辉三角的题,可以直接返回vv,也是因为C++11之后,STL均实现了移动拷贝和移动赋值,否则消耗是巨大的,若没有移动拷贝和移动赋值,通常会传要变化的矩阵的引用过来

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv;
        vv.resize(numRows);//直接在vv中开辟并初始化numRows个
        for(int i = 0;i<numRows;i++)
        {
            vv[i].resize(i+1);//给每一行开
            vv[i][0]=1;
            vv[i][vv[i].size()-1]=1;//第一个和最后一个都给成1
        }
        for(int i = 0;i<vv.size();i++)
        {
            for(int j = 0;j<vv[i].size();j++)
            {
                if(vv[i][j] != 1)
                {
                    vv[i][j] = vv[i-1][j] + vv[i-1][j-1];
                }
            }
        }
        return vv;
    }
};

7、插入操作

在C++11中,STL不仅仅对类增加了移动构造和移动赋值运算符重载,还对每个插入函数增加了右值引用的版本,这里以list的push_back操作为例

int main()
{
	list<cxf::string> lt;
	cxf::string s1("111111111111");
	lt.push_back(s1);
	lt.push_back(cxf::string("2222222222"));
	lt.push_back("7777777");
	return 0;
}

list的push_back会先创建一个新结点,然后用传进来的值去拷贝构造里面对应的值。这里面的lt.push_back(s1)会调用拷贝构造进行深拷贝,而后面两个都是调用移动拷贝,减少了拷贝。

我们现在就来模拟实现以下插入操作使用右值,首先,直接使用之前写的List

namespace cxf
{
	template<class T>
	struct __List_node//创建一个T类型的链表结点
	{
		__List_node(const T& data = T())//构造函数
			:_data(data)
			, _next(nullptr)
			, _prev(nullptr)
		{}

		__List_node<T>* _next;
		__List_node<T>* _prev;
		T _data;
	};
	template<class T, class Ref, class Ptr>
	struct __List_iterator//封装链表的迭代器
	{
		typedef __List_node<T> Node;
		typedef __List_iterator<T, Ref, Ptr> Self;
		Node* _node;//成员变量
		__List_iterator(Node* node)//构造函数。将迭代器中的结点初始化成传过来的结点
			:_node(node)
		{}
		// *it
		Ref operator*()
		{
			return _node->_data;
		}
		Ptr operator->()
		{
			return &_node->_data;
		}
		// ++it
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		// it++
		Self operator++(int)
		{
			Self tmp(*this);//调用默认的拷贝构造,因为是指针类型所以直接用默认的
			//_node = _node->_next;
			++(*this);
			return tmp;
		}
		// --it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		// it--
		Self operator--(int)
		{
			Self tmp(*this);
			//_node = _node->_prev;
			--(*this);
			return tmp;
		}
		// it != end()
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};
	template<class T>
	class List//真正的链表
	{
	public:
		typedef __List_node<T> Node;//将链表结点的名称重命名为Node
		typedef __List_iterator<T, T&, T*> iterator;
		typedef __List_iterator<T, const T&, const T*> const_iterator;
		//带头双向循环链表
		List()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		List(std::initializer_list<T> il)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			for (auto e : il)
				push_back(e);
		}
		~List()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		List(const List<T>& lt)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			//const_iterator it = lt.begin();//这里迭代器不需要指定是那个类域,因为就是在这个类中使用
			//while (it != lt.end())
			//{
			//	push_back(*it);
			//	++it;
			//}
			for (const auto& e : lt)//这里与上面用迭代器一样,因为最终也会被替换成迭代器
				push_back(e);
		}
		/*List<T>& operator=(const List<T>& lt)
		{
			if (this != &lt)
			{
				clear();
				for (const auto& e : lt)
					push_back(e);
			}
			return *this;
		}*/
		List<T>& operator=(List<T> lt)
		{
			swap(_head, lt._head);//原来的空间给这个临时变量,因为这个临时变量是自定义类型,出了作用域后会自动调用析构函数
			return *this;
		}
		void clear()//clear是清除除了头节点意外的所以结点
		{
			iterator it = begin();
			while (it != end())
			{
				erase(it++);
			}
		}
		void push_back(const T& x)//一定要用引用,因为T不一定是内置类型
		{
			/*Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(),x);
		}
		void pop_back()
		{
			/*Node* tail = _head->_prev;
			Node* prev = tail->_prev;
			delete tail;
			_head->_prev = prev;
			prev->_next = _head;*/
			erase(--end());
		}
		void push_front(const T& x)
		{
			/*Node* first = _head->_next;
			Node* newnode = new Node(x);
			_head->_next = newnode;
			newnode->_prev = _head;
			newnode->_next = first;
			first->_prev = newnode;*/
			insert(begin(), x);
		}
		void pop_front()
		{
			/*Node* first = _head->_next;
			Node* second = first->_next;
			delete first;
			_head->_next = second;
			second->_prev = _head;*/
			erase(begin());
		}
		void insert(iterator pos, const T& x)
		{
			Node* newnode = new Node(x);
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}
		void erase(iterator pos)
		{
			assert(pos != end());//不能删除头节点
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			cur = nullptr;
		}
		iterator begin()
		{
			return iterator(_head->_next);//使用这个结点去构造一个迭代器,并将这个迭代器返回
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);//使用这个结点去构造一个迭代器,并将这个迭代器返回
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}
	private:
		Node* _head;
	};
}

我们很容易就能想到对push_back增加一个右值引用的版本,因为push_back中调用了insert,所以insert也到增加一个右值引用的版本

void push_back(const T& x)//一定要用引用,因为T不一定是内置类型
{
	insert(end(),x);
}
void push_back(T&& x)
{
	insert(end(), x);
}
void insert(iterator pos, const T& x)
{
	Node* newnode = new Node(x);
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
}
void insert(iterator pos, T&& x)
{
	Node* newnode = new Node(x);
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	prev->_next = newnode;
	newnode->_prev = prev;
	newnode->_next = cur;
	cur->_prev = newnode;
}

一运行,会发现并没有使用移动构造,这是为什么呢?

因为右值引用本身是左值,insert向下传递时是匹配到了左值引用的版本

为什么右值引用本身是左值呢?

在前面,也可以看到实现移动构造是将右值引用的内容转移了,如果右值引用本身是右值,是转移不了的,所以只能是左值。因为右值引用本身是左值,所以在向下传递时需要move一下

注意,这里是每一次传右值,都需要move,push_back调用insert需要,insert调用new需要,还需要实现一个右值引用版本的构造函数(此时这个构造函数不能有缺省值,否则两个默认构造会出错),并且构造函数里面也需要move

#pragma once
namespace cxf
{
	template<class T>
	struct __List_node//创建一个T类型的链表结点
	{
		__List_node(const T& data = T())//构造函数
			:_data(data)
			, _next(nullptr)
			, _prev(nullptr)
		{}
		__List_node(T&& data)//构造函数
			:_data(move(data))
			, _next(nullptr)
			, _prev(nullptr)
		{}

		__List_node<T>* _next;
		__List_node<T>* _prev;
		T _data;
	};
	template<class T, class Ref, class Ptr>
	struct __List_iterator//封装链表的迭代器
	{
		typedef __List_node<T> Node;
		typedef __List_iterator<T, Ref, Ptr> Self;
		Node* _node;//成员变量
		__List_iterator(Node* node)//构造函数。将迭代器中的结点初始化成传过来的结点
			:_node(node)
		{}
		// *it
		Ref operator*()
		{
			return _node->_data;
		}
		Ptr operator->()
		{
			return &_node->_data;
		}
		// ++it
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		// it++
		Self operator++(int)
		{
			Self tmp(*this);//调用默认的拷贝构造,因为是指针类型所以直接用默认的
			//_node = _node->_next;
			++(*this);
			return tmp;
		}
		// --it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		// it--
		Self operator--(int)
		{
			Self tmp(*this);
			//_node = _node->_prev;
			--(*this);
			return tmp;
		}
		// it != end()
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};
	template<class T>
	class List//真正的链表
	{
	public:
		typedef __List_node<T> Node;//将链表结点的名称重命名为Node
		typedef __List_iterator<T, T&, T*> iterator;
		typedef __List_iterator<T, const T&, const T*> const_iterator;
		//带头双向循环链表
		List()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
		}
		List(std::initializer_list<T> il)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			for (auto e : il)
				push_back(e);
		}
		~List()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		List(const List<T>& lt)
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			//const_iterator it = lt.begin();//这里迭代器不需要指定是那个类域,因为就是在这个类中使用
			//while (it != lt.end())
			//{
			//	push_back(*it);
			//	++it;
			//}
			for (const auto& e : lt)//这里与上面用迭代器一样,因为最终也会被替换成迭代器
				push_back(e);
		}
		/*List<T>& operator=(const List<T>& lt)
		{
			if (this != &lt)
			{
				clear();
				for (const auto& e : lt)
					push_back(e);
			}
			return *this;
		}*/
		List<T>& operator=(List<T> lt)
		{
			swap(_head, lt._head);//原来的空间给这个临时变量,因为这个临时变量是自定义类型,出了作用域后会自动调用析构函数
			return *this;
		}
		void clear()//clear是清除除了头节点意外的所以结点
		{
			iterator it = begin();
			while (it != end())
			{
				erase(it++);
			}
		}
		void push_back(const T& x)//一定要用引用,因为T不一定是内置类型
		{
			/*Node* tail = _head->_prev;
			Node* newnode = new Node(x);
			tail->_next = newnode;
			newnode->_prev = tail;
			newnode->_next = _head;
			_head->_prev = newnode;*/
			insert(end(),x);
		}
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}
		void pop_back()
		{
			/*Node* tail = _head->_prev;
			Node* prev = tail->_prev;
			delete tail;
			_head->_prev = prev;
			prev->_next = _head;*/
			erase(--end());
		}
		void push_front(const T& x)
		{
			/*Node* first = _head->_next;
			Node* newnode = new Node(x);
			_head->_next = newnode;
			newnode->_prev = _head;
			newnode->_next = first;
			first->_prev = newnode;*/
			insert(begin(), x);
		}
		void pop_front()
		{
			/*Node* first = _head->_next;
			Node* second = first->_next;
			delete first;
			_head->_next = second;
			second->_prev = _head;*/
			erase(begin());
		}
		void insert(iterator pos, const T& x)
		{
			Node* newnode = new Node(x);
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}
		void insert(iterator pos, T&& x)
		{
			Node* newnode = new Node(move(x));
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
		}
		void erase(iterator pos)
		{
			assert(pos != end());//不能删除头节点
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			cur = nullptr;
		}
		iterator begin()
		{
			return iterator(_head->_next);//使用这个结点去构造一个迭代器,并将这个迭代器返回
		}
		iterator end()
		{
			return iterator(_head);
		}
		const_iterator begin() const
		{
			return const_iterator(_head->_next);//使用这个结点去构造一个迭代器,并将这个迭代器返回
		}
		const_iterator end() const
		{
			return const_iterator(_head);
		}
	private:
		Node* _head;
	};
}

实际上,左值和右值都是可以自由转换的,因为左值和右值就是语法概念上的区别

void func(const cxf::string& s)
{
	cout << "void func(const cxf::string& s)" << endl;
}
void func(cxf::string&& s)
{
	cout << "void func(cxf::string&& s)" << endl;
}
int main()
{
	cxf::string s1("111111111");
	func(s1);
	func((cxf::string&&)s1);
	func(cxf::string("7777777"));
	func((cxf::string&)cxf::string("7777777"));
	return 0;
}

8、完美转发

当我们在写函数模板时,如果每个模板都要写一个左值引用版本,一个右值引用版本,未免太麻烦,可以直接使用万能引用模板

template<class T>
void Func(T&& x)
{
	// ...
}

当传过来的参数是左值,就会被实例化为左值引用,右值则会被实例化为右值引用。注意,这里能够这样用是因为这是一个函数模板,前面STL的插入操作之所以要写两个版本,是因为那是类模板,类模板实例化之后T就是固定的了。

假设我们现在有下面这个场景,要看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; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	int a;
	PerfectForward(a);					// 左值
	PerfectForward(std::move(a));		// 右值
	const int b = 8;
	PerfectForward(b);					// const 左值
	PerfectForward(std::move(b));		// const 右值
	return 0;
}

因为t是一个引用,所以不能直接使用typeid,只能使用函数。但是上面这样写仍然是有问题的,如果T是左值,没什么问题,如果T是右值,这时t本身是左值引用或const 左值引用,需要move。两种情况就无法处理

要解决这个问题,我们先来看一下这个函数模板会实例化出怎样的函数

void PerfectForward(int& t)
{
	Fun(t);
}
void PerfectForward(int&& t)
{
	Fun(t);
}
void PerfectForward(const int& t)
{
	Fun(t);
}
void PerfectForward(const int&& t)
{
	Fun(t);
}

会实例化出这4个函数,我们要的就是当t是左值引用或const 左值引用时,直接传t,而t是右值引用或const 右值引用时,传move(t),也就是下面这样

void PerfectForward(int& t)
{
	Fun(t);
}
void PerfectForward(int&& t)
{
	Fun(move(t));
}
void PerfectForward(const int& t)
{
	Fun(t);
}
void PerfectForward(const int&& t)
{
	Fun(move(t));
}

此时就可以用到完美转发了

template<typename T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t)); // 完美转发
}

完美转发也是一个模板。作用:保持属性进行传递。也就是先识别一下T,当T是左值时,直接传t,当T是右值时,传move(t)

move和forward本质都是类型转换,move是传入左值返回右值,forward可自行判断

注意,move(t)会比(string&&)t好一些,因为move(t)还能识别t是否为const

前面的类模板中使用move的地方都可以使用forward替代,但这里只能使用forward

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值