C++11新特性(2):深入 C++ 参数传递黑盒:从引用折叠到完美转发,再到可变参数模板

C++11新特性(2):深入 C++ 参数传递黑盒:从引用折叠到完美转发,再到可变参数模板

  • 前言
  • 一、引用折叠
    • [1.1 概念和规则](#1.1 概念和规则)
    • [1.2 应用:万能引用](#1.2 应用:万能引用)
  • 二、完美转发
    • [2.1 完美转发的意义](#2.1 完美转发的意义)
    • [2.2 使用方法和原理](#2.2 使用方法和原理)
  • 三、可变参数模版
    • [3.1 基本语法及原理](#3.1 基本语法及原理)
    • [3.1.1 基本语法](#3.1.1 基本语法)
    • [3.1.2 原理](#3.1.2 原理)
    • [3.2 包扩展](#3.2 包扩展)
      • [3.2.1 递归展开](#3.2.1 递归展开)
      • [3.2.2 参数包展开](#3.2.2 参数包展开)
    • [3.3 list中的emplace_back函数](#3.3 list中的emplace_back函数)
      • [3.3.1 emplace_back函数原型](#3.3.1 emplace_back函数原型)
      • [3.3.2 emplace_back函数和push_back函数的区别](#3.3.2 emplace_back函数和push_back函数的区别)
    • [在这里插入图片描述 我们可以看到:在临时对象构造的时候,emplace_back是要比push_back少一次移动构造,这是为什么呢?](#在这里插入图片描述 我们可以看到:在临时对象构造的时候,emplace_back是要比push_back少一次移动构造,这是为什么呢?)
    • 第一个问题:
    • 第二个问题:
    • 第三个问题:
      • [3.2.3 emplace_back函数的实现](#3.2.3 emplace_back函数的实现)


递归何不归:个人主页
个人专栏 : 《C++庖丁解牛》《数据结构详解》

在广袤的空间和无限的时间中,能与你共享同一颗行星和同一段时光,是我莫大的荣幸

前言

上一篇讲了右值引用,解决了移动语义的问题。

但新问题来了:如何写一个函数,能把收到的参数原样转发给另一个函数?比如实现 emplace_back。

答案就是引用折叠、完美转发和可变参数模板 。这三个特性是现代C++泛型编程的基石,本文带你一一拆解。

一、引用折叠

1.1 概念和规则

  • C++中不能直接定义引用的引用如int& && r = i ;,这样写会直接报错,通过模板或typedef
    中的类型操作可以构成引用的引用。
  • 通过模板或typedef 中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用 ,所有其他组合均折叠成左值引用。

下方是代码示例:

cpp 复制代码
// 由于引用折叠限定,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;
}

我们可以看到:现在对于引用折叠的使用还仅仅局限域于显式指定T的类型,但是这还不是引用折叠的真正用法

1.2 应用:万能引用

引用折叠的真正价值在于根据参数参数推导类型,从而实现"万能引用"

Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int实参是int左值,模板参数T的推导int& ,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。

cpp 复制代码
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;
	// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	// 所以Function内部会编译报错,x不能++
	Function(b);    // const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const 右值


	//如果是没有显示指示Function的类型,那么右值往往是被识别为int类型,而左值往往被识别为引用

	return 0;
}

二、完美转发

2.1 完美转发的意义

我们之前提到过:右值引用表达式的性质是左值,这也就意味在右值作为参数传递时,其会不断地退化成左值,这显然会影响程序执行的效果

那我们是不是可以使用move来将退化的右值重新转换回来呢?

仔细想想,好像也不行 ,这是因为在万能引用的场景中,我们往往是不知道参数类型是左值还是右值的 ,如果"一刀切"的move,也会把本来就是左值的参数也改成右值,这显然会引发严重的错误可能会导致左值的数据被修改(右值传递到最后可能是调用移动构造)

这时,我们想要保持对象的属性,就必须要用到完美转发

2.2 使用方法和原理

使用方式如下所示:

cpp 复制代码
template<class T>
void Function(T&& t)
{
	 Fun(forword<T>t);
}

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

cpp 复制代码
 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(forword<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;
	// b是左值,推导出T为const int&,引用折叠,模板实例化为void Function(const int& t)
	// 所以Function内部会编译报错,x不能++
	Function(b);    // const 左值

	// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&& t)
	// 所以Function内部会编译报错,x不能++
	Function(std::move(b)); // const 右值


	//如果是没有显示指示Function的类型,那么右值往往是被识别为int类型,而左值往往被识别为引用

	return 0;
}

三、可变参数模版

3.1 基本语法及原理

3.1.1 基本语法

我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或
typename...指出接下来的参数表示零或多个类型列表
;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示 ,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则

cpp 复制代码
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;
}

3.1.2 原理

总的来说:

  • 模版:一个函数模版实例化出来多个不同类型参数的函数
  • 可变参数模版:一个可变参数模版实例化出多个参数数量不同的模版函数
cpp 复制代码
// 原理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);

3.2 包扩展

对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。

3.2.1 递归展开

要知道:包展开是在编译阶段发生的事情 ,此时代码并没有开始运行,所以使用if这样的运行时判断是起不到预期效果的

接下来写的包展开函数其实都是通过函数参数匹配的方法来展开的

这也就意味着我们需要三个函数:

1、外层接收函数(接收包)

2、展开操作函数(递归调用自身)

3、结束函数(参数满足条件时调用,结束展开)

cpp 复制代码
// 包扩展(解析出参数包的内容)
void ShowList()
{
	// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}

template <class T, class ...Args>
void ShowList(T&& x, Args&&... args)
{
	// 运行时
	/*if (sizeof...(args) == 0)
		return;*/

	cout << x << " ";
	// args是N个参数的参数包
	// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
	ShowList(args...);
}

template <class ...Args>
void Print(Args&&... args)
{
	ShowList(args...);
}

3.2.2 参数包展开

这种包展开方式在本质上和前面提到的递归展开本质其实是一样的 ,只不过是使用返回的参数的数量判断结束

cpp 复制代码
//包扩展
template <class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}

//template <class T>
//int GetArg(const T& x)
//{
//	cout << x << " ";
//	return 0;
//}

template <class ...Args>
void Arguments(Args... args)
{}

template <class ...Args>
void Print(Args... args)
{
	// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
	Arguments(GetArg(args)...);
}

需要注意的是:以上两种写法都是静态生成的,其生成逻辑就是根据参数匹配调用最符合的函数,当符合终止函数的参数类型时,调用终止函数,结束展开

3.3 list中的emplace_back函数

3.3.1 emplace_back函数原型

可以看到,emplace_back函数的参数是包,参数是不确定的,而push_back的参数是确定的value_type

3.3.2 emplace_back函数和push_back函数的区别

先下结论:

emplace_back适配性更好(可以不用隐式类型转换),效率更高,在某些场景下可以省下一次移动构造

cpp 复制代码
int main()
{
	jyy::list<bit::string> lt;
	// 传左值,跟push_back一样,走拷贝构造
	bit::string s1("111111111111");
	bit::string s2("111111111111");
	cout << "*********************************" << endl;

	lt.emplace_back(s1);
	cout << "*********************************" << endl;

	lt.push_back(s1);
	cout << "*********************************" << endl;

	// 右值,跟push_back一样,走移动构造
	lt.emplace_back(move(s1));
	cout << "*********************************" << endl;

	lt.push_back(move(s2));
	cout << "*********************************" << endl;

	lt.emplace_back("111111111111");
	cout << "*********************************" << endl;

	// 直接传参,隐式类型转换
	lt.push_back("111111111111");
	cout << "*********************************" << endl;

	return 0;
}

我们可以看到:在临时对象构造的时候,emplace_back是要比push_back少一次移动构造,这是为什么呢?

这是因为:

  • emplace_back的参数类型直到传导到最底层才确定 ,所以可以在最后直接构造
  • 但是push_back的参数类型一开始就是确定的,这使得必须先构造成参数类型的对象,这个对象又是临时变量,最后调用移动构造
cpp 复制代码
int main()
{
	jyy::list<pair<bit::string, int>> lt1;
	// 跟push_back一样
	// 构造pair + 拷贝/移动构造pair到list的节点中data上
	pair<bit::string, int> kv("苹果", 1);
	lt1.emplace_back(kv);
	cout << "*********************************" << endl;

	// 21:15
	// 跟push_back一样
	lt1.emplace_back(move(kv));
	cout << "*********************************" << endl;

	// 这里达到的效果是push_back做不到的
	//lt1.emplace_back({ "苹果", 1 }); // 不支持
	lt1.emplace_back("苹果", 1 ); 
	cout << "*********************************" << endl;

	lt1.push_back({ "苹果", 1 });
	cout << "*********************************" << endl;

	return 0;
}

我们就这个场景提出两个问题:

1、为什么emplace_back可以直接传两个对象初始化,push_back不能?

2、为什么emplace_back不可以使用初始化列表

3、为什么push_back要比empalce_back多一个移动构造?

第一个问题:

这是因为push_back只有一个参数,必须传入一个现成的对象,不可以直接传入两个对象,这是因为这样在语法上就无法通过。

第二个问题:

这是因为初始化列表其实就是告诉编译器:这两个类型是不确定的,需要自己来确认 ,但是emplace 的类型也是不确定的 ,两个都不确认,自然是行不通的。

第三个问题:

这是因为empalce_back是最后才确认类型,所以const char* 对象可以一直传递下去 ,直接构造,但是push_back在刚开始就确认了pair中的first是string不将const char 转换成string就无法继续向下传递*,这时候就必须先构造,再移动构造,也就多出来了一个移动构造了。

3.2.3 emplace_back函数的实现

cpp 复制代码
#pragma once
#pragma once
#include<iostream>
#include<list>
#include<cassert>
using namespace std;

namespace bit
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			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);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;
			reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}

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

		//移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			// 转移掠夺你的资源
			swap(s);
		}
		//也就是将资源转移出来,这也是右值引用的类型是左值的原因

		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" <<
				endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;
				reserve(s._capacity);
				for (auto ch : s)
				{
					push_back(ch);
				}
			}
			return *this;
		}

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

		~string()
		{
			//cout << "~string() -- 析构" << endl;
			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];
				if (_str)
				{
					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;
		}

		size_t size() const
		{
			return _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}
namespace jyy
{
	template<class T>
	class list_node
	{
	public:
		//这里还是一个注意的点:const是为了减少拷贝,T()是为了使用默认构造,拥有更好的适配性

		list_node() = default;

		/*template<class X>
		list_node(X&& data = T())
			:_data(forward<X>(data))
			, next(nullptr)
			, prev(nullptr)
		{}*/
		template<class ...Args>
		list_node(Args&&... args)
			:_data(forward<Args>(args)...)
			,next(nullptr)
			,prev(nullptr)
		{ }

		T _data;
		list_node<T>* next;
		list_node<T>* prev;
	};

	template<class T, class Ref, class  Ptr>
	struct list_iterator
	{
		typedef list_node<T> Node;
		typedef list_iterator<T, Ref, Ptr> Self;

		//这里的Self可以非常好的兼容两个类型的迭代器
		list_iterator(Node* node)
			:_node(node)
		{}

		Ref operator*()
		{
			return _node->_data;
		}
		Ptr operator->()
		{
			return &_node->_data;
		}
		Self& operator++()
		{
			_node = _node->next;
			return *this;
		}
		Self& operator--()
		{
			_node = _node->prev;
			return *this;
		}

		Self operator++(int)
		{
			iterator ret = *this;
			_node = _node->next;
			return ret;
		}
		Self operator--(int)
		{
			iterator ret = *this;
			_node = _node->prev;
			return ret;
		}

		bool operator !=(const Self& it)const
		{
			return it._node != _node;
		}
		bool operator ==(const Self& it)const
		{
			return it._node == _node;
		}

	public:
		Node* _node;
	};

	template<class Contianer>
	void printf_contianer(const Contianer& con)
	{

		auto it = con.begin();
		while (it != con.end())
		{
			*it += 10;
			++it;
		}
		cout << endl;

		for (auto e : con)
		{
			cout << e << " ";
		}
		cout << endl;
	}
	
	
	template<class T>
	class list
	{
	public:
		typedef list_node<T> Node;
		/*typedef list_iterator<T> iterator;
		typedef list_const_iterator<T> const_iterator;*/

		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;
			_size = 0;
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			insert(end(), move(x));
		}

		/*iterator insert(iterator it, T&& data)
		{
			Node* new_node = new Node(move(data));

			Node* next = it._node;
			Node* prev = it._node->prev;

			prev->next = new_node;
			new_node->prev = prev;

			next->prev = new_node;
			new_node->next = next;
			++_size;
			return --it;
		}

		iterator insert(iterator it,const T& data)
		{
			Node* new_node = new Node(data);

			Node* next = it._node;
			Node* prev = it._node->prev;

			prev->next = new_node;
			new_node->prev = prev;

			next->prev = new_node;
			new_node->next = next;
			++_size;
			return --it;
		}*/
		///////////////////////////////
		template<class... Args>
		void emplace_back(Args&&... args)
		{
			insert(end(), forward<Args>(args)...);
		}

		void push_front(T x)
		{
			insert(begin(), x);
		}

		void pop_back()
		{
			erase(--end());
		}
		/*template<class X>
		iterator insert(iterator it, X&& data)
		{
			Node* new_node = new Node(forward<X>(data));

			Node* next = it._node;
			Node* prev = it._node->prev;

			prev->next = new_node;
			new_node->prev = prev;

			next->prev = new_node;
			new_node->next = next;
			++_size;
			return --it;
		}*/

		
		template<class... Args>
		iterator insert(iterator it,Args&&... args)
		{
			Node* new_node = new Node(forward<Args>(args)...);

			Node* next = it._node;
			Node* prev = it._node->prev;

			prev->next = new_node;
			new_node->prev = prev;

			next->prev = new_node;
			new_node->next = next;
			++_size;
			return --it;
		}



		iterator erase(iterator it)
		{
			assert(it != end());

			Node* to_delete = it._node;
			Node* next = it._node->next;
			Node* prev = it._node->prev;
			iterator ret = ++it;
			//这里存在bug,会使it指向下一个位置,从而delete错误的位置
			prev->next = next;
			next->prev = prev;

			delete to_delete;

			--_size;
			return ret;
		}

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

}
```
相关推荐
同勉共进1 小时前
并发编程系列(二)—— store, load 与 RMW
c++·arm·并发编程·x86·store·load·rmw
山甫aa1 小时前
多叉树定义与遍历-----从零开始的数据结构
开发语言·c++·二叉树·多叉树
无限进步_2 小时前
【C++】寻找数组中出现次数超过一半的数字:三种解法深度剖析
开发语言·c++·git·算法·leetcode·github·visual studio
深邃-2 小时前
【Web安全】-Kali,Linux配置(1):Kali网络配置,LinuxEnvConfig配置脚本,APT源的讲解,Kali设置中文
linux·运维·开发语言·网络·安全·web安全·网络安全
Hello World . .2 小时前
Linux驱动编程:内核同步的艺术-从互斥到底半部
linux·开发语言·数据库
江山与紫云2 小时前
告别重复造轮子:Codex写脚本
开发语言·python
覆东流2 小时前
第8天:python列表基础
开发语言·python
咸鱼翻身小阿橙2 小时前
C++ 与 QML 交互入门笔记
c++·笔记·交互
Rabitebla2 小时前
二分查找(含有动画展示):不再写出死循环
java·开发语言