C++11之右值引用和移动语义

目录

一、左值和右值的概念

二、左值引用和右值引用

三、引用延长声明周期

四、左值和右值的参数匹配

五、左值引用和右值引用的使用场景

5.1、左值引用的使用场景

5.2、移动构造和移动赋值

5.2.1、移动构造和移动赋值的实现

5.2.2、右值可不可以进行修改

5.2.3、移动构造和移动赋值的实现的例子

5.3、右值引用和移动语义解决传值返回的问题

5.3.1、右值引用和移动语义的实践场景

5.3.2、编译器的优化机制

5.4、右值引用和移动语义在容器插入中的提效

5.4.1、在容器插入中的提效

5.4.2、模拟实现list容器中的插入接口

六、类型分类

七、引用折叠

7.1、引用折叠的概念

7.2、引用折叠和右值引用重载的区分

八、完美转发

8.1、完美转发的概念

8.2、万能引用结合完美转发的使用


一、左值和右值的概念

  • 由于一些历史的原因,很多人认为左值指的就是赋值符号左边的值,右值指赋值符号右边的值,其实这种理解方式是不对的。
  • 左值指的通常是一个变量表达式,就比如变量,指针解引用,它们通常可以长久存在,存储在内存中,并且可以取地址,有const修饰的左值我们不能修改它的值但是可以取它的地址。左值既可以出现在赋值符号的左边也可以出现在赋值符号的右边。
  • 右值指的也是一个表达式,但是右值通常不能长久存在,要么是字面量常量,要么是表达式求值后产生的临时对象,右值是不能取地址的。右值只能出现在赋值符号的右边,不能出现在赋值符号的左边。
  • 像左值被定义为lvalue右值被定义为rvalue,传统上将lvalue解释为left value将rvalue解释为right value,但是在现代c++中lvalue通常被解释为locator value 意思为存储在内存中的,rvalue解释为read value指可以提供数据值但是不能进行寻址的。

二、左值引用和右值引用

  • 前面在学习基础的C++语法的时候我们就已经学习了引用的概念,如果在C++98标准中确实只有一个引用的概念,在现在的C++标准中(C++11)引入了右值引用,所以接下来我们就要区分两种引用了,我们前面学习的是左值引用,接下来我们要学习的是右值引用。
  • int& r1 = x; int&& r2 = 10;第一个语句便是左值引用,第二个语句是右值引用。左值引用只能引用左值,右值引用只能引用右值。
  • 如果使用move函数对一个左值进行强转后,也可以用右值引用进行引用。move函数是库里的一个函数模板本质上就是将左值强制类型转化为右值。其实move还涉及一些其他的知识,这里了解一下他的本质就可以。
  • 需要注意的是变量表达式的值都是左值,这意味着一个右值被右值引用绑定后,右值引用变量就变成了左值的属性了。不用对这种规定表示奇怪,C++委员会这样设计是为了解决下面要介绍的移动构造的问题。后面会详细介绍。
  • 右值引用和左值引用一样都是引用,都只是在语法成面上不开空间,是一个对象的别名,在底层实现上都是用指针实现的。
int main()
{
	int a = 10; // a为左值
	const int b = a; // b为const修饰的左值
	string s("1111");// s为左值
	s[0] = 'X'; // s[0]为左值
	double x = 1.1, y = 2.3;
	x + y; // x + y会产生一个临时对象,为右值

	int& r1 = a; // 左值引用
	const int& r2 = b; // const左值引用
	string& r3 = s; // 左值引用

	int&& r4 = 10; // 右值引用
	double&& r5 = x + y; // 右值引用
  string&& r6 = move(s);
	return 0;
}

三、引用延长声明周期

看下面一段代码:

int main()
{
	bit::string("11111");
	cout << "====================================" << endl;
	return 0;
}

生命周期只有当前行。

int main()
{
	bit::string&& r1 = bit::string("11111");
	cout << "====================================" << endl;
	return 0;
}

可以发现右值引用可以延长生命周期。不要感觉奇怪,如果语法规则不这样进行设计,就会造成野引用的出现。

同样const左值引用也可以延长临时对象的生命周期。但前提是必须得加上const,因为临时对象具有常性,我们需要加上const来控制缩小它的权限。

int main()
{
	const bit::string& r1 = bit::string("11111");
	cout << "====================================" << endl;
	return 0;
}

四、左值和右值的参数匹配

  • C++98针对于左值和右值的传参问题只需要设计一个参数为const左值引用的即可,这样既可以传左值也可以传右值。
  • C++11以后分别重载了左值引用,const左值引用,右值引用,如果没有右值引用,那么左值和右值都可以传const左值引用,如果有了右值引用,编译器会将右值优先选择传右值引用。也就是说编译器遵循有更合适的一定传更合适的,没有完全匹配的使用const左值引用来凑合一下也是可以的。
void f(int& x)
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
	std::cout << "const 的左值引用重载 f(" << x << ")\n";
}
int main()
{
	int i = 1;
	const int ci = 2;
	int&& x = 1;
	f(i); // 调用 f(int&) 
	f(ci); // 调用 f(const int&) 
	f(3); // 调用 f(const int&) 
	f(std::move(i)); // 调用 调用 f(const int&)
	return 0;
}

这是没有重载右值引用,所以左值调用左值引用,const左值调用const左值引用,右值也调用左值引用。

void f(int& x)
{
	std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
	std::cout << "const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
	std::cout << "右值引用重载 f(" << x << ")\n";
}

int main()
{
	int i = 1;
	const int ci = 2;
	int&& x = 1;

	f(i); // 调⽤ f(int&) 
	f(ci); // 调⽤ f(const int&) 
	f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&) 
	f(std::move(i)); // 调⽤ f(int&&)
	return 0;
}

当重载了右值引用后编译器就会调用更加匹配的,所以如果传右值都会调用右值引用。

五、左值引用和右值引用的使用场景

左值引用和右值引用的使用场景都将基于下面这两个类进行分析。

class Solution01 {
public:
	// 传值返回需要拷⻉ 
	string addStrings(string num1, string num2) {
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位 
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		return str;
	}
};
class Solution02 {
public:
	// 这⾥的传值返回拷⻉代价就太⼤了 
	vector<vector<int>> generate(int numRows) {
		vector<vector<int>> vv(numRows);
		for (int i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < numRows; ++i)
		{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
		}
		return vv;
	}
};

针对上面的两个函数在C++98中如果写出这样的代码,是非常容易挨批的,原因就是效率太低了,第一个效率还好,像第二个这样的vector中嵌套了一个vector如果这样进行传值返回效率是非常低的。C++98的解决办法就是传输出型参数。来到了C++11,引入右值引用就是为了解决这个问题的,解决办法难道是使用右值引用返回吗,显然这种做法是错误的,原因就是str作为局部对象,在函数调用结束的时候已经销毁,如果继续使用引用来引用它会形成野引用。

5.1、左值引用的使用场景

如果一个对象他的生命并不会随着该函数栈帧的销毁而销毁,那么我们便可以使用传左值引用返回,这样来减少拷贝,提升效率。常见的场景有实现operator[],operator++等等。这里不再一一赘述。

5.2、移动构造和移动赋值

我们其实已经知道了C++11引入右值引用就是为了解决上面代码效率低的问题,但是右值引用不是作为返回值来解决这个问题的,那么是怎么来解决问题的呢?其实主要就是依赖于这个右值引用,我们都知道又是都是那些临时对象,即将析构的值,如果一个对象作为右值我们还使用深拷贝去拷贝它,这完全是在给我们的程序增加负担,如果我们可以在这个右值即将析构的时候将它的值"窃取"(转移)过来给我们使用,不就不用拷贝了吗,实现这种"窃取"的行为其实就是通过移动构造和移动赋值这两个默认的成员函数实现的。

其实在前面学习类和对象的知识的时候就已经提到了C++的类会提供6个默认成员函数。前面我们已经学习了4个,今天学习完右值引用之后,我们便可以了解一下剩下的两个移动构造和移动赋值。

  • 移动构造是一种特殊的构造函数,类似于拷贝构造,它要求第一个参数必须是右值引用,如果还有其他的参数则必须要有缺省值。
  • 移动赋值是一种特殊的赋值运算符重载,它与拷贝赋值构成函数重载,它的要求是他的参数要为右值引用。
  • 如果你自己没有实现移动构造且析构函数,拷贝构造,拷贝赋值中的任意一个,编译器会默认生成一个移动构造,默认生成的移动构造对于内置类型会对内置类型进行逐字节的拷贝,对于自定义类型会调用它的移动构造,如果没有移动构造就会调用它的拷贝构造。
  • 如果你没有实现移动赋值且没有实现析构函数,拷贝构造,拷贝赋值中的任意一个,编译器就会默认生成一个移动赋值,默认生成的移动赋值对于内置类型会对内置类型进行逐字节的拷贝,对于自定义类型会调用它的移动赋值,如果它没有移动赋值就调用它的拷贝赋值。
  • 下面根据这两个特点举个例子:
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:
	bit::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

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:
	bit::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);
	return 0;
}

因为右值通常是临时对象等即将析构的对象,所以当我们使用右值去构造对象和对对象进行赋值的时候编译器就去匹配移动构造和移动赋值,所以说移动构造和移动赋值本质上是通过窃取引用的右值的资源来提高效率的。

5.2.1、移动构造和移动赋值的实现

// 移动构造
string::string(string&& s)
{
	std::cout << "string(string&& s)" << " -> " << "移动构造" << std::endl;
	swap(s);// 将即将析构的s的值转移到this身上
}
// 移动赋值
string& string::operator=(string&& s)
{
	std::cout << "string& operator=(string&& s)" << " -> " << "移动赋值" << std::endl;
	swap(s);// 将即将析构的s的值转移到this身上
	return *this;
}

5.2.2、右值可不可以进行修改

看完上面的介绍之后,我相信大家应该对右值引用和移动语义有所了解了。肯定有的人是有疑问的,就是这个右值不是具有常性吗,它不是不能进行修改吗,那么既然它不能进行修改,为什么它可以进行资源的交换呢?

答:的确,C++98有明确的规定,像临时对象,匿名对象,字面量都是具有常性的,都不可以进行修改。但是C++11引入了右值引用又有了如下规定:一个右值被右值引用绑定后,右值引用变量就变成了左值的属性了。前面说的这个规定在这里做了解释,这样的规定既没有改变右值不可以修改的本质,也解释了右值的引用可以修改的问题。

5.2.3、移动构造和移动赋值的实现的例子

int main()
{
	bit::string s1("xxxxx");
	// 拷贝构造 
	bit::string s2 = s1;
	// 构造+拷贝构造,优化后直接构造 
	bit::string s3 = bit::string("yyyyy");
	// 拷贝构造
	bit::string s4 = move(s1);
	cout << "******************************" << endl;

	return 0;
}

运行结果:

// 移动构造
string::string(string&& s)
{
	std::cout << "string(string&& s)" << " -> " << "移动构造" << std::endl;
	swap(s);
}
// 移动赋值
string& string::operator=(string&& s)
{
	std::cout << "string& operator=(string&& s)" << " -> " << "移动赋值" << std::endl;
	swap(s);
	return *this;
}
int main()
{
	bit::string s1("xxxxx");
	// 拷贝构造 
	bit::string s2 = s1;
	// 构造+移动构造,优化后直接构造 
	bit::string s3 = bit::string("yyyyy");
	// 移动构造 
	bit::string s4 = move(s1);
	cout << "******************************" << endl;

	return 0;
}

5.3、右值引用和移动语义解决传值返回的问题

我们使用上面提供的addStrings这个函数进行演示,如下:

下面的代码编译器会做特殊处理,就是将str识别为右值。

5.3.1、右值引用和移动语义的实践场景

移动语义是C++11引入的一个新的特性,它的目的是实现高效的资源转移,避免不必要的拷贝,提高程序的性能。它的实现机制就是通过"窃取"右值的资源来解决不必要的拷贝。

这幅图是没有移动构造和移动赋值,只有拷贝构造和拷贝赋值的情况

这幅图是有移动构造没有移动赋值的情况

这幅图是没有移动构造和移动赋值,只有拷贝构造和拷贝赋值的情况

实现了移动赋值的情况

5.3.2、编译器的优化机制

在一个步骤里面构造+拷贝构造或者拷贝构造+拷贝构造,会优化成直接构造或者一次拷贝构造。在C++17的标准中严格规定了编译器要这样执行。

像那种三合一的操纵,是编译器自己的实现机制,并不是C++标准的规定。但是这种优化是有迹可循的。

图片中左边这种str是ret的引用,也就是说,str直接在ret的内存地址上进行构造,实现零拷贝。

右边这种情况类似,str是临时对象的别名,str在临时对象的内存上进行构造。

这二者的底层实现都是类似的都是记录一下地址,在该地址上进行构造。

5.4、右值引用和移动语义在容器插入中的提效

看完上面的优化以后是不是感觉右值引用和移动语义没有啥意义了,明明编译器会进行优化,为什么我还要大费周章的搞这个右值引用和移动语义呢?

答:第一点:虽然编译器会进行优化,但是像第二种优化机制并不是标准委员会规定的,而是编译器自己实现的,也就是说不同的编译器的优化机制是不一样的,像VS2022是这样优化的,但是如果放到其他编译器上跑同样的代码的话,效率的高低就不能确定了。

第二点:我们知道C++是一个追求极致效率的语言,既然引入了右值引用和移动构造,那么它一定是为了提升效率而存在的,绝对不会因为编译器的优化导致这个语法没有存在的必要。他的用处还有就是在容器插入数据的时候的提效。

看下面几幅图:

我们可以看到C++11将所有的插入的成员函数都增加了一个右值引用的版本,很显然这个右值引用的版本就是用来提效的。接下了我们以list容器为基础进行演示。

5.4.1、在容器插入中的提效

右值引用和移动语义在容器插入中的提效机制是这样的,如果我传过来的是一个左值,那我就乖乖的拷贝一个对象进行插入,如果我传过来的是一个右值,那我就调用移动构造去将这个右值的数据转移到我身上。本质上还是调用移动构造,进而减少没必要的拷贝次数,实现提效。

int main()
{
	std::list<bit::string> lt;
	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}

5.4.2、模拟实现list容器中的插入接口

模拟实现的list没有实现右值引用重载后的push和insert函数运行下面代码

int main()
{
	Code_Journey::list<bit::string> lt;
	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}

实现右值引用的重载接口

  • 实现的细节,注意在实现插入接口的右值引用的重载时,由于它们都是调用了insert所以一定要逐层去增加右值引用的接口。
  • 由于右值引用引用右值后它的属性会变成左值,所以在像下层去传参的时候一定要将右值引用的变量使用move强转一下,只有这样才能调到移动构造,否则调的依然时拷贝构造。
#pragma once
#include<iostream>

namespace Code_Journey
{
	// 结点的类
	template<class T>
	struct list_node {
		typedef T value_type;
		value_type _data;
		list_node* _next;
		list_node* _prve;
		list_node(const value_type& x)
			:_data(x)
			, _next(nullptr)
			, _prve(nullptr)
		{}
		list_node(value_type&& x = value_type())
			:_data(move(x))
			, _next(nullptr)
			, _prve(nullptr)
		{}
		// 同样不需要写析构函数
		// 成员变量都没有在堆区申请空间
	};
	// 迭代器的类
	template<class T, class Ref, class Ptr>
	struct list_iterator {
		typedef T value_type;
		typedef list_node<value_type> Node;
		typedef list_iterator<value_type, Ref, Ptr> Self;

		// 不一定创建了结点就要析构
		// 这里根本没有在堆区申请空间
		Node* _node;

		list_iterator(Node* node)
			:_node(node)
		{
		}
		// 浅拷贝就可以满足我们的需求
		/*list_iterator(const list_iterator& lt)
		{
			cout << "list_iterator(list_iterator& lt)" << endl;
			_node = lt._node;
		}*/
		// 本质就是重载*
		Ref operator*()
		{
			return _node->_data;
		}
		Ptr operator->()
		{
			return &(_node->_data);
		}
		// 后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		// 前置++
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		// 后置--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prve;
			return tmp;
		}
		// 前置--
		Self& operator--()
		{
			_node = _node->_prve;
			return *this;
		}
		bool operator!=(const Self& it) const
		{
			return _node != it._node;
		}
		bool operator==(const Self& it) const
		{
			return _node == it._node;
		}
	};
	// const迭代器的类
	//template<class T>
	//struct const_list_iterator {
	//	typedef T value_type;
	//	typedef list_node<value_type> Node;
	//	typedef const_list_iterator<value_type> Self;

	//	// 不一定创建了结点就要析构
	//	// 这里根本没有在堆区申请空间
	//	Node* _node;

	//	const_list_iterator(Node* node)
	//		:_node(node)
	//	{}
	//	// 浅拷贝就可以满足我们的需求
	//	/*list_iterator(const list_iterator& lt)
	//	{
	//		cout << "list_iterator(list_iterator& lt)" << endl;
	//		_node = lt._node;
	//	}*/
	//	// 本质就是重载*
	//	const value_type& operator*()
	//	{
	//		return _node->_data;
	//	}
	//	// 后置++
	//	Self operator++(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_next;
	//		return tmp;
	//	}
	//	// 前置++
	//	Self& operator++()
	//	{
	//		_node = _node->_next;
	//		return *this;
	//	}
	//	// 后置--
	//	Self operator--(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_prve;
	//		return tmp;
	//	}
	//	// 前置--
	//	Self& operator--()
	//	{
	//		_node = _node->_prve;
	//		return *this;
	//	}
	//	bool operator!=(const Self& it) const
	//	{
	//		return _node != it._node;
	//	}
	//	bool operator==(const Self& it) const
	//	{
	//		return _node == it._node;
	//	}
	//};
	// list的类
	template<class T>
	class list {
		typedef T value_type;
		typedef list_node<value_type> Node;

	public:
		typedef list_iterator<value_type, value_type&, value_type*> iterator;
		typedef list_iterator<value_type, const value_type&, const value_type*> const_iterator;

		list()
		{
			empty_init();
		}
		void empty_init()
		{
			_head = new Node;
			_head->_next = _head;
			_head->_prve = _head;

			_size = 0;
		}
		// 列表初始化(没必要传引用,initializer_list的成员变量就只有两个指针)
		list(initializer_list<value_type> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		list(list<value_type>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}
		void swap(list<value_type>& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		// 不能+const
		list<value_type>& operator=(list<value_type> lt)
		{
			swap(lt);
			return *this;
		}
		void clear()
		{
			// C链表写法
			/*Node* cur = _head->_next;
			while (cur != _head)
			{
				Node* next = cur->_next;
				delete cur;
				cur = next;
			}*/
			// 迭代器写法
			auto it = begin();
			while (it != end())
			{
				it = erase(it);
				it++;
			}
		}
		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}
		// 返回普通的迭代器
		iterator begin()
		{
			// 支持隐式类型转换
			// return _head->_next;
			return iterator(_head->_next);
		}
		// 返回普通的迭代器
		iterator end()
		{
			// return _head;
			return iterator(_head);
		}
		// 返回cosnt的迭代器
		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}
		// 返回cosnt的迭代器
		const_iterator end() const
		{
			return const_iterator(_head);
		}
		size_t size() const
		{
			return _size;
		}
		void push_back(const value_type& x)
		{
			// new一个新结点
			//Node* node = new Node(x);
			// 尾插 _head node head->prve
			/*node->_prve = _head->_prve;
			node->_next = _head;
			_head->_prve->_next = node;
			_head->_prve = node;*/

			// 复用
			// 尾插就相当于在_head的_prve位置进行insert
			//Node* cur = _head;
			//insert(cur, x);
			insert(end(), x);
		}
		void push_back(value_type&& x)
		{
			// new一个新结点
			//Node* node = new Node(x);
			// 尾插 _head node head->prve
			/*node->_prve = _head->_prve;
			node->_next = _head;
			_head->_prve->_next = node;
			_head->_prve = node;*/

			// 复用
			// 尾插就相当于在_head的_prve位置进行insert
			//Node* cur = _head;
			//insert(cur, x);
			insert(end(), move(x));
		}
		void pop_back()
		{
			// 删除这个位置的前一个位置
			erase(--end());
		}
		void push_front(const value_type& x)
		{
			insert(begin(), x);
		}
		void push_front(value_type&& x)
		{
			insert(begin(), move(x));
		}
		void pop_front()
		{
			erase(begin());
		}
		iterator insert(iterator pos, const value_type& x)
		{
			Node* newnode = new Node(x);
			Node* cur = pos._node;
			Node* prve = cur->_prve;
			newnode->_next = cur;
			newnode->_prve = cur->_prve;
			prve->_next = newnode;
			cur->_prve = newnode;

			++_size;
			return pos;
		}
		iterator insert(iterator pos, value_type&& x)
		{
			Node* newnode = new Node(move(x));
			Node* cur = pos._node;
			Node* prve = cur->_prve;
			newnode->_next = cur;
			newnode->_prve = cur->_prve;
			prve->_next = newnode;
			cur->_prve = newnode;

			++_size;
			return pos;
		}
		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* next = cur->_next;
			// cur->_prve cur cur->_next
			cur->_prve->_next = cur->_next;
			cur->_next->_prve = cur->_prve;
			delete cur;
			cur = nullptr;

			--_size;
			//return next;(隐士类型转化)
			return iterator(next);//(匿名对象)
		}
	private:
		Node* _head;
		size_t _size;
	};
}

六、类型分类

C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。

纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、 true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。

将亡值是指返回右值引用的函数调用的表达式和转换为右值引用的转换函数的调用表达,如 move(x)、static_cast(x)

泛左值(generalizedvalue,简称glvalue),泛左值包含将亡值和左值。

七、引用折叠

7.1、引用折叠的概念

如果我们这样写程序int& && r = i;编译器会直接报错的,也就是说编译器并不允许直接定义引用的引用,但是如果是通过typedef和模板参数构成的引用的引用,编译器是允许的。

通过模板或typedef出的类型可以与引用构成引用的引用,C++11针对这样的操作给出了一个引用折叠的概念,就是右值引用的右值引用折叠成右值引用,其它的所有情况都折叠成左值引用。

像f2这个函数模板,基于引用折叠的规则,当我们传递右值的时候他会实例化成右值引用,当我们传递左值的时候他会实例化成左值引用,将这样的函数模板的参数称为万能引用。

就比如下面的程序:

typedef int& lvref;
typedef int&& rvref;
int main()
{
	int a = 10;
	lvref& r1 = a; // 左值引用碰左值引用为左值引用
	rvref&& r2 = 10; // 右值引用碰右值引用为右值引用
	lvref&& r3 = a; // 左值引用碰右值引用为右值引用
	return 0;
}
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤ 
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤ 
template<class T>
void f2(T&& x)
{}
int main()
{
 typedef int& lref;
 typedef int&& rref;
 int n = 0;
 lref& r1 = n; // r1 的类型是 int& 
 lref&& r2 = n; // r2 的类型是 int& 
 rref& r3 = n; // r3 的类型是 int& 
 rref&& r4 = 1; // r4 的类型是 int&& 
 // 没有折叠->实例化为void f1(int& x) 
 f1<int>(n);
 f1<int>(0); // 报错 
 // 折叠->实例化为void f1(int& x) 
 f1<int&>(n);
 f1<int&>(0); // 报错 
 // 折叠->实例化为void f1(int& x) 
 f1<int&&>(n);
 f1<int&&>(0); // 报错 
 // 折叠->实例化为void f1(const int& x) 
 f1<const int&>(n);
 f1<const int&>(0);
 // 折叠->实例化为void f1(const int& x) 
 f1<const int&&>(n);
 f1<const int&&>(0);
 // 没有折叠->实例化为void f2(int&& x) 
 f2<int>(n); // 报错 
 f2<int>(0);
 // 折叠->实例化为void f2(int& x) 
 f2<int&>(n);
 f2<int&>(0); // 报错 
 // 折叠->实例化为void f2(int&& x) 
 f2<int&&>(n); // 报错 
 f2<int&&>(0);
 return 0;
}

7.2、引用折叠和右值引用重载的区分

看下面一段代码:你认为它们是不是万能引用呢?

void push_front(value_type&& x)
{
	insert(begin(), move(x));
}
iterator insert(iterator pos, value_type&& x)
{
	Node* newnode = new Node(move(x));
	Node* cur = pos._node;
	Node* prve = cur->_prve;
	newnode->_next = cur;
	newnode->_prve = cur->_prve;
	prve->_next = newnode;
	cur->_prve = newnode;

	++_size;
	return pos;
}

不要看到参数是泛型的右值引用就以为都是万能引用,实际上它们并不是万能引用,只是右值引用的函数重载。为什么这样说呢,原因很简单就是万能引用时根据我们传入的参数可以自行推断模板参数的类型,也就是说它应该是一个函数模板,而这里的两个函数都是我们模拟实现的list的成员函数,它们的模板参数已经被我们显示实例化了,所以说这并不是万能引用。

八、完美转发

8.1、完美转发的概念

引用折叠通常会结合完美转发一起使用。

Function(T&& t)函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化以后是右值引用的Function函数。

但是结合我们前面的讲解,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发实现。

完美转发forward本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面示例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引用返回;传递给 Function的实参是左值,T被推导为int&,引用折叠为左值引用,forward内部t被强转为左值引用并返回。

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
	return static_cast<_Ty&&>(_Arg);
}
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<class T>
void Function(T&& t)
{
	Fun(t);
	//Fun(forward<T>(t));
}
int main()
{
	// 10是右值,推导出T为int,模板实例化为void Function(int&& t) 
	Function(10); // 右值 
	int a;
	// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) 
	Function(a); // 左值 
	// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) 
	Function(std::move(a)); // 右值 
	const int b = 8;
	// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int& t)
	Function(b); // const 左值 
	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	Function(std::move(b)); // const 右值 
	return 0;
}

8.2、万能引用结合完美转发的使用

那么既然引用这折叠可以直接使用万能引用,那么为什么我们还要实现两份插入接口呢?实际上C++11才引入的右值引用,之前版本的左值引用版本就已经存在了,所以库中就直接增加以一个右值引用的版本。

下面我们使用引用折叠和完美转发来实现重新实现一下list中的插入接口。

template<class X>
list_node(X&& x)
	:_data(forward<X>(x))
	,_next(nullptr)
	,_prve(nullptr)
{}
template<class X>
void push_back(X&& x)
{
	insert<X>(end(), forward<X>(x));
}
template<class X>
void push_front(X&& x)
{
	insert(begin(), forward<X>(x));
}
template<class X>
iterator insert(iterator pos, X&& x)
{
	Node* newnode = new Node(forward<X>(x));
	Node* cur = pos._node;
	Node* prve = cur->_prve;
	newnode->_next = cur;
	newnode->_prve = cur->_prve;
	prve->_next = newnode;
	cur->_prve = newnode;

	++_size;
	return pos;
}
### C++引用移动语义的概念及用法 #### 定义与区别 在C++中,左指的是具有持久存储的对象,可以通过名称访问。而则是临时对象或字面量,在表达式结束后即销毁。为了区分这两种不同的实体,C++引入了两种不同类型的引用:左引用(`T&`)引用(`T&&`)。引用允许绑定到即将消亡的资源,从而实现高效的资源转移。 #### 实现机制 当定义一个接受引用参数的函数时,意味着该函数期望接收的是一个临时对象或者是显式标记为可被“掠夺”的对象。这使得可以在不创建额外副本的情况下直接接管这些对象内部的数据成员,进而提高性能并减少不必要的内存分配开销[^2]。 #### `std::move()` 的作用 `std::move()` 是一种用于将某个变量转换成形式的标准库工具。它并不会真正改变原对象的状态,而是告诉编译器这个对象可以作为对待,以便触发相应的优化逻辑—比如调用类中的移动构造函数而不是复制构造函数。需要注意的是,虽然名为 “move”,但它本身并不执行任何实际的动作;真正的数据迁移发生在后续的操作过程中,取决于目标类型是否支持以及如何实现了对应的接口方法[^1]。 ```cpp #include <iostream> using namespace std; class MyClass { public: string data; // 默认拷贝构造函数 MyClass(const MyClass &other):data(other.data){ cout << "Copy constructor called." << endl; } // 移动构造函数 MyClass(MyClass &&tmp) noexcept : data(move(tmp.data)){ cout << "Move constructor called." << endl; } }; int main(){ MyClass obj{"example"}; MyClass movedObj = move(obj); } ``` 上述代码展示了自定义类 `MyClass` 如何利用移动语义来提升效率。当实例化第二个对象 `movedObj` 并使用 `std::move()` 将第一个对象传递给它的时候,会优先尝试调用带有引用形参版本的构造函数来进行初始化工作。如果一切顺利,则可以直接占有前者持有的字符串而不必再做一次完整的深拷贝操作[^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值