前言

对于C++11新特性:前文介绍的auto的具体使用、nullptr指针空值、范围for的使用,我们在以前文章已有讲解05【C++ 入门基础】内联、auto、指针空值,以前提前讲解是为了让我们更快的将好用的语法使用起来,这里也不重复。

本文介绍的是:可变模板参数


1. 可变模板参数

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,现阶段,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们浅尝辄止。

首先我们知道可变参数,C语言中经典的可变参数就是printf/scanf: 它们允许传入多个参数!!
在这里插入图片描述
这里的printf就叫函数的可变参数,这种形式可以给函数传多个参数。
其原理类似于将后面参数存入一个数组中。

那么类比函数,我们的模板的可变参数的需求,就是要对一个模板传入多个参数。
我们模版的可变参数,就叫 — 模版参数的参数包

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

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持我们对参数包使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

1.1 参数包获取方式

(1)递归函数方式展开参数包

//这两个模版实例化出的函数构成函数重载:

// 递归终止函数
template <class T>
void ShowList(const T& t)    //如果单参数的调用,则会优先匹配这个函数模版
{
	cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args) //如果多参数,优先匹配带参数包的这个.
{
	cout << value << " ";
	ShowList(args...);
}

int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	//ShowList();   //报错: “ShowList”: 未找到匹配的重载函数
	return 0;
}

当我们调用ShowList(1, 'A', std::string("sort"));
1.如果参数包中的值还有多个,则会继续匹配我们带val的这个ShowList函数,正常走递归逻辑,每次递归第一个变量会传参给val,剩余的参数会作为参数包,然后在函数体中重复递归调用,从而将参数包的所有参数都打印出来。
2.如果我们的参数包中只剩一个值,则它会优先匹配我们不带val的ShowList函数,它就不是一个递归函数,可以正常结束,正常结束后我们上层带val的递归ShowList函数才结束。

这种是递归不断地分解参数包的方法。

我们上面如果不传参调用,就会出现报错,原因是虽然我们的模板参数包可以接收无参数,但是我们写死的value不可以,它至少需要一个参数去推导模板的类型,面对这种情况我们可以多重载一个非模板接受空参数的函数:

void ShowList()
{
	cout << endl;
}
如果没有val如何取值

在我们库中的一些函数的参数只接收参数包,没有val,这又是怎么取出来的呢?
在这里插入图片描述
我们可以多套一层:

void ShowList()          // 4. 同时我们也可以加上接受无参数的非模版函数重载,去解决调用无参调用的情况
{
	cout << endl;
}
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
	cout << t << endl;   // 3. 当参数只剩一个,则会匹配到我们单参数的模版函数这里.
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
	cout << value << " ";
	ShowList(args...);   // 2. 展开函数这里又会优先匹配自己进行递归.
}
// 入口函数
template <class ...Args>
void ShowList(Args... args)
{
	ShowList(args...);  // 1. 我们多参数会优先匹配展开函数
}

int main()
{
	ShowList(1, 2, 3, 4);
	ShowList();    //如果不加一个无参函数匹配,这里匹配到入口函数处,将会出现无限递归,程序会错误码返回 : (进程 11104)已退出,代码为 -1073741571.
	return 0;
}

(2)包展开表达式

在探究这个方式之前,我们来了解一下参数包的传递原理。


参数包传递原理
template <class ...Args>
void ShowList(Args... args)
{
	ShowList(args...);  // 1. 我们多参数会优先匹配展开函数
}

在上面代码中,我们在参数包后面加入了一个“…”,并将它传递给了函数ShowList。
核心原理:编译时的参数列表生成与解包​

  1. ​模板实例化是编译时行为:​​ 当你调用一个像 ShowList(1, “hello”, 3.14)的函数时,编译器会进行模板实例化。它看到你传递了三个参数:一个 int,一个 const char*,一个 double。

  2. Args…捕获参数类型:​​ 在实例化 ShowList<Args…>时,Args这个模板参数包会被实例化为一个包含 int, const char*, double的类型列表。你可以把 Args想象成一个编译时的类型元组。

  3. args捕获参数值:​​ 函数参数 Args… args中的 args是一个函数参数包。在实例化后的函数签名中,它会被展开为具体的参数列表:int arg0, const char* arg1, double arg2。编译器会为每个传递进来的参数生成一个对应的具名变量 (arg0, arg1, arg2)。args就是这个参数列表的打包表示。

  4. “…”是解包/展开操作符:​​ 在函数内部使用 args…时,末尾的 …是关键。它告诉编译器:“请把参数包 args在这里展开成它包含的所有参数”。
    当编译器看到 ShowList(args…);时,它首先将 args展开为 arg0, arg1, arg2。
    然后,它尝试调用 ShowList(arg0, arg1, arg2)。

参数包通过 …展开,是编译器在编译阶段的行为,在编译时,编译器会将 args…​直接替换为它所包含的所有参数,​用逗号分隔。和我们人手传递展开没有本质区别。

注意:

参数包是 C++ 模板元编程的核心特性,完全在编译时处理。所以参数包通过 …展开的过程 ​与 std::initializer_list没有直接关系,参数包不是展开成了 std::initializer_list。


其实我们的包展开表达式,和我们正常的包传递展开的原理类似。
看看使用方式:

template<class T>
int printArgs(T x)
{
	cout << x << " ";
	return 0;
}
template<class ...Args>
void print(Args ...args)
{
	int a[] = { printArgs(args)... }; //输出: 1 2.2 你好
	//{printArgs(args)...};  //语法错误: 此处出现意外的“...”;应为“;”
}
int main()
{
	print(1, 2.2, "你好");
	return 0;
}

这里的 …不是直接跟在 args后面作为函数调用的参数展开,而是作用在 printArgs(args)这个表达式上。

​含义:​​ 它表示“对参数包 args中的每一个元素,分别计算表达式 printArgs(element)”。
原理:它和我们正常包传递展开的原理类似,也是编译器在编译阶段的行为,和手动写开没有本质区别。
​结果:​​ 如果 args包含 arg0, arg1, arg2,那么 printArgs(args)…会被编译器展开为 printArgs(arg0), printArgs(arg1), printArgs(arg2)。

为什么下面那行报错了呢?
​特别注意:​​

printArgs(args)…;单独作为一个语句在语法上是无效的,因为它展开后是多个表达式语句 (printArgs(arg0); printArgs(arg1); …),C++ 不允许在一行写多个独立的表达式语句。你需要把它放在允许逗号分隔表达式列表的上下文中,比如:

  1. 函数参数列表:SomeFunc(printArgs(args)…)展开为 SomeFunc(printArgs(arg0), printArgs(arg1), printArgs(arg2));
  2. 初始化列表:std::vector vec = { printArgs(args)… }展开为std::vector vec = { printArgs(arg0), printArgs(arg1), printArgs(arg2) };
配合逗号运算符

既然我们的“…”可以作用在表达式上,那么我们可以搭配逗号运算符去使用:

template<class T>
void printArgs(T x)
{
	cout << x << " ";
	//return 0;    //如果搭配逗号表达式,我们的printArgs就不用返回0了,因为返回0,是我们int a[]数组初始化时的需要,这里逗号表达式已经给了。
}
template<class ...Args>
void print(Args ...args)
{
	int a2[] = { (printArgs(args), 0)... };
}

int a[] = { (printArgs(args), 0)… } 展开为int a[] = { (printArgs(arg0), 0),(printArgs(arg1),(printArgs(arg2) }。

针对表达式的展开和我们正常包传递展开的原理类似,也是编译器在编译阶段的行为,和手动写开没有本质区别。
​如果 args包含 arg0, arg1, arg2,那么 printArgs(args)…会被编译器展开为 printArgs(arg0), printArgs(arg1), printArgs(arg2)。

1.2 参数包配合构造函数使用

前面提到了我们的包展开表达式可以在函数参数列表中使用,那么和其同原理的参数包展开就更不用说了,可以在搭配构造函数中使用,给出一个简单的例子:

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 2025,int month = 6,int day = 6)
		:_year(year)
		,_month(month)
		,_day(day)
	{
	}
	void print()
	{
		cout << _year << " " << _month << " " << _day << endl;
	}
};
template<class ...Args>
Date CrateDate(Args ...args)
{
	Date d(args...);
	return d;
}
//template<class ...Args>   //在堆上申请更常用
//Date* CrateDate(Args ...args)
//{
//	return new Date(args...);
//}
int main()
{
	Date a = CrateDate(1888, 88, 88);
	a.print();   //输出: 1888 88 88

	Date b = CrateDate(1888, 66);
	b.print();   //输出: 1888 66 6

	Date c = CrateDate(1888);
	c.print();   //输出: 1888 6 6

	Date d = CrateDate();
	d.print();   //输出: 2025 6 6
	
	Date e = CrateDate(a);
	e.print();   //输出: 1888 88 88
	return 0;
}

1.3 sizeof取参数包大小

我们在sizeof后面加入…便可以得出参数包的元素个数,即sizeof...(args);

注意:我们的参数包不可以像数组一样这样取值,会报错。

template <class ...Args>
void ShowList(Args... args)
{
	int n = sizeof...(args);
	cout << n << endl;
	for (int i = 0; i < n; i++)   //报错: “args”: 必须在此上下文中扩展参数包
	{
		cout << args[i] << " ";
	}
}

int main()
{
	ShowList(1, 2, 3, 4);
	return 0;
}

总结

  1. 可变参数模板能够让您创建可以接受可变参数的函数模板和类模板。
  2. 参数包的获取方式有两种:

(1) 递归函数方式获取
(2) 包展开表达式,包展开表达式和我们普通的包传递的原理是一样的,都是编译器在编译阶段自动的对参数包展开,只不过包展开表达式面向的是表达式。

  1. 参数包还可以配合构造函数使用,使用模板函数接收到的参数包,可用解包操作符“…”传递给类的构造函数。
  2. sizeof…()可以计算出参数包中类型元素的个数。
  3. 可变模版参数我们了解其用法即可。

本文章为作者的笔记和心得记录,顺便进行知识分享,有任何错误请评论指点 😃。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐