首页 > 编程语言 >C++11可变参数模板实例详解

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

来源:互联网 2026-07-16 06:58:11

C++11引入可变参数模板,使函数模板能接受任意数量、任意类型的参数。定义时用class...或typename...表示类型包,Args...args表示形参包,并通过sizeof...获取参数个数,极大增强了模板的通用性与灵活性。

前言

这篇专题适合已经学过C++的同学复习使用,重点巩固可变参数模板的相关知识。

一、可变模板参数

1.1. 可变参数模板的概念

可变参数模板是C++11引入的新特性,它使参数的高度泛化成为可能——我们终于可以创建能够接受可变参数的函数模板和类模板了。

长期稳定更新的攒劲资源: >>>点此立即查看<<<

  • 在C++11之前,无论是类模板还是函数模板,里面的模板参数个数都是固定的。由于可变参数模板本身比较抽象,使用起来确实需要一些技巧。
  • 实际上,C++11之前C++也有可变参数的概念,比如printf函数可以接收任意多个参数。但需要注意,那是函数参数的可变参数,并非模板的可变参数

注:本篇只涉及函数模板的可变参数,类模板的内容不在此讨论。

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

函数的可变参数模板定义起来比较直观,请看以下格式:

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

举个例子:

template
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
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
void ShowList(T1 arg1);
template
void ShowList(T1 arg1,T2 arg2);
template
void ShowList(T1 arg1, T2 arg2,T3 arg3);

需要特别强调的是:语法并不支持用 args[i] 的方式来获取参数包中的参数。下面的写法是错误的:

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

因此,要获取参数包中的各个参数,只能通过展开参数包的方式实现。常用的展开方式有两种:递归展开逗号表达式展开

1.3. 参数包的展开方式

1.3.1. 递归展开参数包

递归展开的核心思路如下:

  • 为函数模板多增加一个模板参数,这样就能从参数包中分离出第一个参数。
  • 在函数模板内部递归调用自身,传入剩下的参数包。
  • 这样层层递归下去,每次分离出一个参数,直到参数包被取完。

例如,想要打印调用函数时传入的各个参数,可以这样写:

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

接下来需要解决的是:递归如何终止?

编写无参的递归终止函数

我们可以再写一个无参的同名函数作为递归终止条件:

//递归终止函数
void ShowList()
{
	cout << endl;
}
//展开函数
template
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;
}

当参数包中的参数个数为0时,递归调用就会匹配到那个无参的终止函数,递归结束。

当然,也可以换一种写法,把ShowListPrint分开来:

void ShowList()
{
	cout << endl;
} 
template 
void ShowList(T x, Args... args)
{
	cout << x << " ";
	ShowList(args...);
} 
// 编译时递归推导解析参数
template 
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
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
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}
template
void Arguments(Args... args)
{}
template
void Print(Args... args)
{
	Arguments(GetArg(args)...);
}
int main()
{
	Print(1, string("2345"), 1.1);
	return 0;
}

其本质是:编译器在编译时会实例化出以下函数:

template
const T& GetArg(const T& x)
{
	cout << x << " ";
	return x;
}
template
void Arguments(Args... args)
{}
template
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_frontpush_backinsert,都增加了对应的emplace_frontemplace_backemplace函数。如下所示:

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

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

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

emplace系列接口的使用方式

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

以list容器的emplace_backpush_back为例:

  • 调用push_back插入元素时,可以传入左值对象或者右值对象,也可以使用列表进行初始化。
  • 调用emplace_back插入元素时,同样可以传入左值对象或者右值对象,但不能使用列表进行初始化
  • 此外,emplace系列接口最大的特点是:插入元素时可以传入用于构造元素的参数包。

emplace系列接口的意义

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

  • 如果调用emplace系列接口时传入的是左值对象,那么首先需要先调用构造函数实例化出一个左值对象,然后在定位new表达式调用构造函数对空间进行初始化时,会匹配到拷贝构造函数
  • 如果传入的是右值对象,那么会先调用构造函数实例化出一个右值对象,然后定位new表达式调用构造函数时,会匹配到移动构造函数
  • 如果传入的是参数包,那么可以直接调用函数进行插入,最终在定位new表达式调用构造函数初始化空间时,匹配到的是构造函数

总结如下:

  • 传入左值对象:构造函数 + 拷贝构造函数
  • 传入右值对象:构造函数 + 移动构造函数
  • 传入参数包:只需要调用构造函数

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

实际上,emplace系列接口的一部分功能与容器原有的插入接口是重叠的。因为容器原有的push_backpush_frontinsert函数也提供了右值引用版本的接口。如果调用这些接口时传入的是右值对象,最终也会调用对应的移动构造函数来移动资源。

emplace接口的意义:

  • emplace系列接口最大的特点是支持传入参数包,用这些参数包直接构造出对象,从而减少一次拷贝。这也是许多人认为emplace系列接口更高效的原因。
  • 但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
int main()
{
	list> mylist;
	pair kv(1, "one");
	mylist.emplace_back(kv);                              //传左值
	cout << endl;
	mylist.emplace_back(pair(2, "two")); //传右值
	cout << endl;
	mylist.emplace_back(3, "three");                      //传参数包
	return 0;
}

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

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。