C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++可变参数模版

C++11可变参数模版实例详解

作者:小松鼠0519

可变参数模版是 C++11 新增的特性之一,它对参数高度泛化能够让我们创建可以接受可变参数的函数模版和类模版,本文介绍C++11可变参数模版的相关知识,感兴趣的朋友跟随小编一起看看吧

前言

注:这个是博主复习使用的专题,仅适用于自己以及学习过C++知识点的同学。

一、可变模版参数

1.1. 可变参数模版的概念

        可变参数模版是 C++11 新增的特性之一,它对参数高度泛化能够让我们创建可以接受可变参数的函数模版和类模版。

注:本篇博客只讲解函数模板的可变参数。

1.2. 可变参数模版的定义方法

        函数的可变参数模板定义方式如下:

template<class...Args>
返回类型 函数名(Args...args)
{
    // 函数体
}

例如:

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

注:用省略号来指出一个模版参数或函数参数的表示一个包,在模版参数列表中,class... 或typename... 指出接下来的参数表示0或多个类型列表;在函数参数列表中,类型名后面跟... 指出接下来表示0或者多个形参对象列表;函数参数包可以用左值引用或右值引用表示。每个参数实例化都遵循引用折叠规则。

现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。比如:

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1,'A');
	ShowList(1,'A',string("hello"));
	return 0;
}

我们可以在函数模板中通过sizeof计算参数包中参数的个数。比如:

template<class... Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	ShowList();							// 0		
	ShowList(1);						// 1
	ShowList(1,'A');					// 2
	ShowList(1,'A',string("hello"));	// 3
	return 0;
}

        但是我们无法直接获取参数包中的每个参数,只能通过 展开参数包的方式 来获取,这是使用可变参数模版的一个主要特点,也是最大的难点。

        他的本质有两种:

1. 结合引用折叠规则实例化出下面的函数。

void ShowList();
void ShowList(int arg1);
void ShowList(int arg1,char arg2);
void ShowList(int arg1,char arg2,string arg3);

2. 更本质的是生成下面的模版参数。

void ShowList();
template<class T1>
void ShowList(T1 arg1);
template<class T1,class T2>
void ShowList(T1 arg1,T2 arg2);
template<class T1, class T2,class T3>
void ShowList(T1 arg1, T2 arg2,T3 arg3);

        注:语法并不支持使用 args[i] 的方式来获取参数包中的参数:
 

template<class... Args>
void ShowList(Args... args)
{
	for (int i = 0; i < sizeof...(args); i++)
	{
		cout << args[i] << " ";
	}
	cout << endl;
}

 这个是错的。

因此要获取参数包中的各个参数,只能通过展开参数包的方式来获取,一般我们会通过递归或逗号表达式来展开参数包。

1.3. 参数包的展开方式

1.3.1. 递归展开参数包

递归展开参数包的方式如下:

例如打印调用函数时传入的各个参数,那么函数模版可以这样子编写:

//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //递归调用,将参数包继续向下传
}

这时我们面临的问题就是,如何终止函数的递归调用。

编写无参的递归终止函数

我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。如下:

//递归终止函数
void ShowList()
{
	cout << endl;
}
//展开函数
template<class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " "; //打印分离出的第一个参数
	ShowList(args...);    //递归调用,将参数包继续向下传
}
int main()
{
	ShowList();							
	ShowList(1);						
	ShowList(1, 'A');					
	ShowList(1, 'A', string("hello"));	
	return 0;
}

        这样一来,当递归调用 ShowList 函数模版时,如果传入的参数包中的参数个数为0,就会直接匹配到这个无参的递归终止函数,就是递归的结束函数。

        当然也可以按照下面的写:

void ShowList()
{
	cout << endl;
} 
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	ShowList(args...);
} 
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}
int main()
{
	Print(1);
	Print(1, 'A');
	Print(1, 'A', string("hello"));
	return 0;
}

1.3.2. 逗号表达式展开参数包

1. 通过列表获取参数包中的参数

        如果参数包中各个参数的类型都是整型,那么也可以把这个参数包放到列表当中初始化这个整型数组,此时参数包中参数就放到数组中了。比如:

template<class...Args>
void Print(Args...args)
{
	int arr[] = { args...};
	for (auto e : arr)
	{
		cout << e << " ";
	}
	cout << endl;
}
int main()
{
	Print(1);
	Print(1,2);
	Print(1,2,3);
	return 0;
}

        但C++并不像Python这样的语言,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList函数时传入的参数只能是整型的,并且还不能传入0个参数,因为数组的大小不能为0,因此我们还需要在此基础上借助逗号表达式来展开参数包。

2. 通过逗号表达式展开参数包

        虽然我们不能用不同类型的参数去初始化一个整型数组,但我们可以借助逗号表达式。

        这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整型值作为返回值来初始化整型数组。比如:

template<class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}
template<class...Args>
void Arguments(Args... args)
{}
template<class... Args>
void Print(Args... args)
{
	Arguments(GetArg(args)...);
}
int main()
{
	Print(1, string("2345"), 1.1);
	return 0;
}

他的本质其实是:
编译器在编译的是偶,实例化下面的函数:

template<class T>
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}
template<class...Args>
void Arguments(Args... args)
{}
template<class... Args>
void Print(int x,string y,double z)
{
	Arguments(GetArg(x),GetArg(y),GetArg(z));
}
int main()
{
	Print(1, string("2345"), 1.1);
	return 0;
}

1.4. STL容器汇总的emplace相关接口函数

        C++11标准给STL中的容器增加emplace版本的插入接口,比如list容器的push_front、push_back和insert函数,都增加了对应的emplace_front、emplace_back和emplace函数。如下:

注意: emplace系列接口的可变模板参数类型都带有“&&”,这个表示的是万能引用,而不是右值引用。

emplace系列接口的使用方式

emplace系列接口的使用方式与容器原有的插入接口的使用方式类似,但又有一些不同之处。

以list容器的emplace_back和push_back为例:

emplace系列接口的意义

        由于emplace系列接口的可变模版参数类型都是万能引用,因此既可以接收左值对象,也可以接收右值对象,还可以接收参数包。

总结一下:

        当然,这里的前提是容器中存储的元素所对应的类,是一个需要深拷贝的类并且该类实现了移动构造函数。否则调用emplace系列接口时,传入左值对象和传入右值对象的效果都是一样的,都需要调用一次构造函数和一次拷贝构造函数。

        实际emplace系列接口的一部分功能和原有各个容器插入接口是重叠的,因为容器原有的push_back、push_front和insert函数也提供了右值引用版本的接口,如果调用这些接口时如果传入的是右值对象,那么最终也是会调用对应的移动构造函数进行资源的移动的。

emplace接口的意义:

验证

        如果要验证我们上述对emplace系列接口的说法,需要借助一个深拷贝的类,下面模拟实现了一个简化版的string类,类当中只编写了我们需要用到的成员函数。     

namespace xxhh
{
	class string
	{
	public:
		//构造函数
		string(const char* str = "")
		{
			cout << "string(const char* str) -- 构造函数" << endl;
			_size = strlen(str); //初始时,字符串大小设置为字符串长度
			_capacity = _size; //初始时,字符串容量设置为字符串长度
			_str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0')
			strcpy(_str, str); //将C字符串拷贝到已开好的空间
		}
		//交换两个对象的数据
		void swap(string& s)
		{
			//调用库里的swap
			::swap(_str, s._str); //交换两个对象的C字符串
			::swap(_size, s._size); //交换两个对象的大小
			::swap(_capacity, s._capacity); //交换两个对象的容量
		}
		//拷贝构造函数(现代写法)
		string(const string& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;
			string tmp(s._str); //调用构造函数,构造出一个C字符串为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=(const string& s) -- 深拷贝" << endl;
			string tmp(s); //用s拷贝构造出对象tmp
			swap(tmp); //交换这两个对象
			return *this; //返回左值(支持连续赋值)
		}
		//移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}
		//析构函数
		~string()
		{
			//delete[] _str;  //释放_str指向的空间
			_str = nullptr; //及时置空,防止非法访问
			_size = 0;      //大小置0
			_capacity = 0;  //容量置0
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

        由于我们在string的构造函数、拷贝构造函数和移动构造函数当中均打印了一条提示语句,因此我们可以通过控制台输出来判断这些函数是否被调用。

        下面我们用一个容器来存储模拟实现的string,并以不同的传参形式调用emplace系列函数。比如:

#include<list>
int main()
{
	list<pair<int, xxhh::string>> mylist;
	pair<int, xxhh::string> kv(1, "one");
	mylist.emplace_back(kv);                              //传左值
	cout << endl;
	mylist.emplace_back(pair<int, xxhh::string>(2, "two")); //传右值
	cout << endl;
	mylist.emplace_back(3, "three");                      //传参数包
	return 0;
}

到此这篇关于十二、C++11可变参数模版的文章就介绍到这了,更多相关C++可变参数模版内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文