引用折叠


C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef中的类型操作可以构成引⽤的引⽤。

通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规 则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个一个仔细理解⼀下。
像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左 值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的
Function,实参是右值,实例化出右值引⽤版本形参的Function。
// 由于引⽤折叠限定,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;

}
template<class T>
void Function(T&& t)
{
	int a = 0;
	T x = a;
	//x++;
	cout << &a << endl;
	cout << &x << endl << endl;
}
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内部会编译报错,x不能++
	Function(b);
	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const 右值
	return 0;
}

完美转发


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

但是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定 后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。
template <class T> T&& forward (typename remove_reference<T>::type&arg);
template <class T> T&& forward (typename remove_reference<T>::type&& arg);
完美转发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;
}

可变参数模板


C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称 为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函

数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class...或
typename...指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。

这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。

template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	double x = 2.2;
	Print(); // 包⾥有0个参数
	Print(1); // 包⾥有1个参数
	Print(1, string("xxxxx")); // 包⾥有2个参数
	Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
	return 0;
}
// 原理1:编译本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理2:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能⽀持
// 这⾥的功能,有了可变参数模板,我们进⼀步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
void Print();
template <class T1>
void Print(T1&& arg1);
template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);
template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
// ...

包扩展


对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个 包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元

素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层
的实现细节如图所⽰。
C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。

empalce系列接⼝


template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position, Args&&... args);
C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上 兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container<T>,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下
std::forward<Args>(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。

#include<list>
// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{
	list<bit::string> lt;
	// 传左值,跟push_back⼀样,⾛拷⻉构造
	bit::string s1("111111111111");
	lt.emplace_back(s1);
	cout << "*********************************" << endl;
	// 右值,跟push_back⼀样,⾛移动构造
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;
	// 直接把构造string参数包往下传,直接⽤string参数包构造string
	// 这⾥达到的效果是push_back做不到的
	lt.emplace_back("111111111111");
	cout << "*********************************" << endl;
	list<pair<bit::string, int>> lt1;
	// 跟push_back⼀样
	// 构造pair + 拷⻉/移动构造pair到list的节点中data上
	pair<bit::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << "*********************************" << endl;
	// 跟push_back⼀样
	lt1.emplace_back(move(kv));
	cout << "*********************************" << endl;
	////////////////////////////////////////////////////////////////////
	// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
	// 这⾥达到的效果是push_back做不到的
	lt1.emplace_back("苹果", 1);
	cout << "*********************************" << endl;
	return 0;
}
namespace bit
{
	template<class T>
	struct ListNode
	{
		ListNode<T>* _next;
		ListNode<T>* _prev;
		T _data;
		ListNode(T&& data)
			:_next(nullptr)
			, _prev(nullptr)
			, _data(move(data))
		{}
		template <class... Args>
		ListNode(Args&&... args)
			: _next(nullptr)
			, _prev(nullptr)
			, _data(std::forward<Args>(args)...)
		{}
	};
	template<class T, class Ref, class Ptr>
	struct ListIterator
	{
		typedef ListNode<T> Node;
		typedef ListIterator<T, Ref, Ptr> Self;
		Node* _node;
		ListIterator(Node* node)
			:_node(node)
		{}
		// ++it;
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}
		Ref operator*()
		{
			return _node->_data;
		}
		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}
	};
	template<class T>
	class list
	{
		typedef ListNode<T> Node;
	public:
		typedef ListIterator<T, T&, T*> iterator;
		typedef ListIterator<T, const T&, const T*> const_iterator;
		iterator begin()
		{
			return iterator(_head->_next);
		}
		iterator end()
		{
			return iterator(_head);
		}
		void empty_init()
		{
			_head = new Node();
			_head->_next = _head;
			_head->_prev = _head;
		}
		list()
		{
			empty_init();
		}
		void push_back(const T& x)
		{
			insert(end(), x);
		}
		void push_back(T&& x)
		{
			insert(end(), move(x));
		}
		iterator insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			// prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			return iterator(newnode);
		}
		iterator insert(iterator pos, T&& x)
			Node* cur = pos._node;
		Node* newnode = new Node(move(x));
		Node* prev = cur->_prev;
		// prev newnode cur
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = cur;
		cur->_prev = newnode;
		return iterator(newnode);
	}
	template <class... Args>
	void emplace_back(Args&&... args)
	{
		insert(end(), std::forward<Args>(args)...);
	}
	// 原理:本质编译器根据可变参数模板⽣成对应参数的函数
	/*void emplace_back(string& s)
	{
	insert(end(), std::forward<string>(s));
	}
	void emplace_back(string&& s)
	{
	insert(end(), std::forward<string>(s));
	}
	void emplace_back(const char* s)
	{
	insert(end(), std::forward<const char*>(s));
	}
	*/
	template <class... Args>
	iterator insert(iterator pos, Args&&... args)
	{
		Node* cur = pos._node;
		Node* newnode = new Node(std::forward<Args>(args)...);
		Node* prev = cur->_prev;
		// prev newnode cur
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = cur;
		{
			cur->_prev = newnode;
			return iterator(newnode);
		}
	private:
		Node* _head;
	};
}
// Test.cpp
#include"List.h"
// emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
int main()
{
	bit::list<bit::string> lt;
	// 传左值,跟push_back⼀样,⾛拷⻉构造
	bit::string s1("111111111111");
	lt.emplace_back(s1);
	cout << "*********************************" << endl;
	// 右值,跟push_back⼀样,⾛移动构造
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;
	// 直接把构造string参数包往下传,直接⽤string参数包构造string
	// 这⾥达到的效果是push_back做不到的
	lt.emplace_back("111111111111");
	cout << "*********************************" << endl;
	bit::list<pair<bit::string, int>> lt1;
	// 跟push_back⼀样
	// 构造pair + 拷⻉/移动构造pair到list的节点中data上
	pair<bit::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << "*********************************" << endl;
	// 跟push_back⼀样
	lt1.emplace_back(move(kv));
	cout << "*********************************" << endl;
	////////////////////////////////////////////////////////////////////
	// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
	// 这⾥达到的效果是push_back做不到的
	lt1.emplace_back("苹果", 1);
	cout << "*********************************" << endl;
	return 0;
}

新的类功能


默认的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重 载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意 ⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调 ⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
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;
}
成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显⽰在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化。
defult和delete
C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显⽰指定移动构造⽣成。
如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加=delete即可,该语法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
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(Person&& p) = default;
	//Person(const Person& p) = delete;
private:
	bit::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	return 0;
}

lambda表达式


lambda表达式语法

lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接
lambda 对象。
lambda表达式的格式: [capture-list] (parameters)-> return type {
function boby }
[capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数⽤,捕捉列表可以传值和传引⽤捕捉,具体细节7.2中我们再细讲。捕捉列表为空也不能省略。
(parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此
部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
{function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以
使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
int main()
{
	// ⼀个简单的lambda表达式
	auto add1 = [](int x, int y)->int {return x + y; };
	cout << add1(1, 2) << endl;
	// 1、捕捉为空也不能省略
	// 2、参数为空可以省略
	// 3、返回值可以省略,可以通过返回对象⾃动推导
	// 4、函数题不能省略
	auto func1 = []
		{
			cout << "hello bit" << endl;
			return 0;
		};
	func1();
	int a = 0, b = 1;
	auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap1(a, b);
	cout << a << ":" << b << endl;
	return 0;
}
捕捉列表

	cout << func2() << endl;
	// 隐式引⽤捕捉
	// ⽤了哪些变量就捕捉哪些变量
	auto func3 = [&]
		{
			a++;
			c++;
			d++;
		};
	func3();
	cout << a << " " << b << " " << c << " " << d << endl;
	// 混合捕捉1
	auto func4 = [&, a, b]
		{
			//a++;
			//b++;
			c++;
			d++;
			return a + b + c + d;
		};
	func4();
	cout << a << " " << b << " " << c << " " << d << endl;
	// 混合捕捉1
	auto func5 = [=, &a, &b]
		{
			a++;
			b++;
			/*c++;
			d++;*/
			return a + b + c + d;
		};
	func5();
	cout << a << " " << b << " " << c << " " << d << endl;
	// 局部的静态和全局变量不能捕捉,也不需要捕捉
	static int m = 0;
	auto func6 = []
		{
			int ret = x + m;
			return ret;
		};
	// 传值捕捉本质是⼀种拷⻉,并且被const修饰了
	// mutable相当于去掉const属性,可以修改了
	// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷⻉
	auto func7 = [=]()mutable
		{
			a++;
			b++;
			c++;
			d++;
			return a + b + c + d;
		};
	cout << func7() << endl;
	cout << a << " " << b << " " << c << " " << d << endl;
	return 0;
}

lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉。

第⼀种捕捉⽅式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,
y, &z] 表⽰x和y值捕捉,z引⽤捕捉。
第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=表⽰隐式值捕捉,在捕捉列表
写⼀个&表⽰隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些
变量。
第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显⽰捕捉。[=, &x]表⽰其他变量隐式值捕捉,
x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是
&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必
须是引⽤捕捉。
lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,
mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以
修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
本篇文章就分享到这里,下篇文章我们一起来聊聊 lambda 的使用场景和它的原理;下篇见!

更多推荐