【C++11】可变参数模板

文章目录

  • [1. 什么是可变参数模板?](#1. 什么是可变参数模板?)
  • [2. 递归函数方式展开参数包](#2. 递归函数方式展开参数包)
  • [3. 逗号表达式展开参数包](#3. 逗号表达式展开参数包)
  • [4. 不使用逗号表达式展开参数包](#4. 不使用逗号表达式展开参数包)
  • [5. 可变参数模板在类中的运用](#5. 可变参数模板在类中的运用)
  • [6. STL容器中的 empalce 相关接口函数](#6. STL容器中的 empalce 相关接口函数)

1. 什么是可变参数模板?

C++11 的新特性【可变参数模板】能够让你创建可以接受【可变参数的函数模板和类模板】,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了。

下面就是一个基本可变参数的函数模板

cpp 复制代码
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数 args 前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为【参数包】,它里面包含了 0 到 N(N >= 0)个模版参数。

cpp 复制代码
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << sizeof...(args) << endl; // 查看参数包的个数
}

int main()
{
	ShowList(1); // 1传给value, 参数包有0个
	ShowList(1, 2); // 1传给value, 2传给参数包
	ShowList(1, 2, 3.3); // 1传给value, 2 和 3.3 传给参数包

	return 0;
}

结果如下:

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

由于语法不支持使用 args[i] 这样方式获取可变参数,所以我们的用一些特殊的方式获取参数包的值。

2. 递归函数方式展开参数包

如何取出参数包的内容呢?

下面就是一种方法,叫做:编译递归,通过第一个参数不断的去推导参数包的值。

代码如下:

cpp 复制代码
template <class T>
void ShowList(T value)
{
	// 结束条件的函数
	cout << value << " ";
	cout << endl;
}

template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);
}

int main()
{
	ShowList(1); 
	ShowList(1, 2); 
	ShowList(1, 2, 3.3); 

	return 0;
}

结果如下:

那么基于此,我们可以实现一个 C++ 类型的 printf 函数,代码如下:

cpp 复制代码
void _ShowList()
{
	// 结束条件的函数
	cout << endl;
}

template <class T, class ...Args>
void _ShowList(T value, Args... args)
{
	cout << value << " ";
	_ShowList(args...);
}

// args代表 0-N 的参数包
template <class ...Args>
void cppPrint(Args... args)
{
	_ShowList(args...);
}

int main()
{
	cppPrint();
	cppPrint(1);
	cppPrint(1, 2);
	cppPrint(1, 2, 3.3);

	return 0;
}

结果如下:

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

这种展开参数包的方式,不需要通过递归终止函数,是直接在 cppPrint 函数体中展开的,其中 PrintArg 不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。

这种就地展开参数包的方式,实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

代码如下:

cpp 复制代码
// 重载(为了打印0个参数)
void cppPrint()
{
	cout << endl;
}

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}

//展开函数
template <class ...Args>
void cppPrint(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}

int main()
{
	cppPrint();
	cppPrint(1);
	cppPrint(1, 2);
	cppPrint(1, 2, 3.3);
	cppPrint(1, 2, 3.3, string("hello world"));

	return 0;
}

结果如下:

cppPrint 函数中的逗号表达式:(PrintArg(args), 0),也是按照这个执行顺序:

  • 先执行 PrintArg(args),再得到逗号表达式的结果 0
  • 同时还用到了 C++11 的另外一个特性 ------ 初始化列表,通过初始化列表来初始化一个变长数组,{ (PrintArg(args), 0)... } 将会展开成 ((PrintArg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为 0 的数组 int arr[sizeof...(Args)]
  • 由于是逗号表达式,在创建数组的过程中,会先执行逗号表达式前面的部分 PrintArg(args) 打印出参数,也就是说在构造 int 数组的过程中就将参数包展开了,这个数组的目的纯粹是为了,在数组构造的过程展开参数包。

4. 不使用逗号表达式展开参数包

我们对代码稍加改动一下,就可以避免使用逗号表达式:

cpp 复制代码
// 重载(为了打印0个参数)
void cppPrint()
{
	cout << endl;
}

template <class T>
int PrintArg(T t)
{
	cout << t << " ";

	return 0;
}

//展开函数
template <class ...Args>
void cppPrint(Args... args)
{
	int arr[] = { PrintArg(args)... }; // 参数包有几个值, 就要调几次这个PrintArg函数
	cout << endl;
}

int main()
{
	cppPrint();
	cppPrint(1);
	cppPrint(1, 2);
	cppPrint(1, 2, 3.3);
	cppPrint(1, 2, 3.3, string("hello world"));

	return 0;
}

结果如下:

5. 可变参数模板在类中的运用

展示了如何通过 可变参数模板 创建不同构造参数的 Date 对象,并使用 动态内存分配 和 拷贝构造函数。

cpp 复制代码
class Date
{
public:
	Date(int y = 1, int m = 1, int d = 1)
		: _year(y), _month(m), _day(d)
	{}

	// 拷贝构造函数
	Date(const Date& other)
		: _year(other._year), _month(other._month), _day(other._day)
	{}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

template <class ...Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...);
	return ret;
}

int main()
{
	// 参数包是3个参数, 而Date类的构造函数的参数也是3个, 刚好匹配上了
	Date* p1 = Create();
	Date* p2 = Create(2025);
	Date* p3 = Create(2025, 12);
	Date* p4 = Create(2025, 12, 12);

	p1->Print();
	p2->Print();
	p3->Print();
	p4->Print();

	Date d(2026, 1, 1);
	Date* p5 = Create(d); // p5是去调用拷贝构造, 我们没有写, 那么编译器会默认生成
	p5->Print();

	return 0;
}

结果如下:

重点:

  • 可变参数模板:使得函数 Create 可以处理不同数量的参数,创建 Date 对象。
  • 默认构造函数:Date 类提供了一个默认构造函数,可以创建默认日期。
  • 拷贝构造函数:当使用已有的 Date 对象(如 d)来创建新的 Date 对象时,会使用拷贝构造函数。

6. STL容器中的 empalce 相关接口函数

来看看 vector 和 list 中的接口:

接口代码如下:

cpp 复制代码
template <class... Args>
void emplace_back(Args&&... args);

首先我们看到的 emplace 系列的接口,支持模板的可变参数,并且万能引用。那么它相对 insertemplace 系列接口的优势到底在哪里呢?

emplace_back支持可变参数,拿到构建 pair 对象的参数后自己去创建对象,那么在这里我们可以看到除了用法上,和 push_back 没什么太大的区别。代码如下:

cpp 复制代码
int main()
{
	std::list< std::pair<int, char> > mylist;
	
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	mylist.emplace_back(make_pair(30, 'c'));

	mylist.push_back(make_pair(40, 'd'));
	mylist.push_back({ 50, 'e' });

	for (auto e : mylist)
		cout << e.first << ":" << e.second << endl;

	return 0;
}

结果如下:

下面我们试一下我们自己写的,带有【拷贝构造】和【移动构造】的 edc::string,再试试呢

cpp 复制代码
namespace edc
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(const char* str)" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			//string tmp(s._str);
			//swap(tmp);
		}

		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;

			swap(s);
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		string& operator=(string&& s)
		{
			cout << "string& operator=(string && s) -- 移动拷贝" << endl;
			swap(s);

			return *this;
		}

		~string()
		{
			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];
				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)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

主函数代码如下:

cpp 复制代码
int main()
{
	std::list< std::pair<int, edc::string> > mylist;

	mylist.emplace_back(10, "sort");
	mylist.emplace_back(make_pair(20, "sort"));

	mylist.push_back(make_pair(30, "sort"));
	cout << "-----------------------" << endl;
	mylist.push_back({ 40, "sort" });

	return 0;
}

结果图一:

结果图二:

通过上面的结果,我们会发现其实差别也不到,emplace_back 是直接构造了,而 push_back 是先构造,再移动构造,其实也还好。

再来看一个例子,还是拿日期类为例:

cpp 复制代码
class Date
{
public:
	Date(int y = 1, int m = 1, int d = 1)
		: _year(y), _month(m), _day(d)
	{
		cout << "Date()构造" << endl;
	}

	Date(const Date& d)
		: _year(d._year), _month(d._month), _day(d._day)
	{
		cout << "Date()拷贝构造" << endl;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

主函数代码如下:

cpp 复制代码
int main()
{
	std::list<Date> lt;
	
	Date d(2025, 12, 12);
	lt.push_back(d); // push_back只能传日期类对象

	// 把参数包一路往下传, 直接去构造或者拷贝构造结点中的日期类对象
	lt.emplace_back(d); // 可以传日期类对象
	lt.emplace_back(2026, 11, 11); // 也可以传日期类对象的参数包

	return 0;
}

结果图一:

结果图二:

以上就是可变参数模板的全部内容。

相关推荐
lihao lihao1 小时前
二分查找
java·数据结构·算法
代码栈上的思考1 小时前
消息队列持久化:文件存储设计与实现全解析
java·前端·算法
sg_knight2 小时前
设计模式实战:策略模式(Strategy)
java·开发语言·python·设计模式·重构·架构·策略模式
麦麦鸡腿堡2 小时前
JavaWeb_SpringBootWeb,HTTP协议,Tomcat快速入门
java·开发语言
码云数智-园园2 小时前
前端跨域全解析:核心原理、解决方案选型与实战指南
开发语言
qq_417695052 小时前
内存对齐与缓存友好设计
开发语言·c++·算法
2301_816651222 小时前
实时系统下的C++编程
开发语言·c++·算法
一然明月2 小时前
Qt QML 锚定(Anchors)全解析
java·数据库·qt