商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > C++11可变参数模版实例详解

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

  发布于2026-07-13 阅读(0)

扫一扫,手机访问

前言

说明一下,这篇专题是复习用的,比较适合已经学过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可变参数模版实例详解

本文转载于:https://www.jb51.net/program/36731742l.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注