📝前言:
这篇文章我们来讲讲C++11——可变参数模板:
🎬个人简介:努力学习ing
📋个人专栏:C++学习笔记
🎀CSDN主页 愚润求学
🌄其他专栏:C语言入门基础,python入门基础,python刷题专栏,Linux
文章目录
- 一,什么是可变参数模板
- 二,基本语法
- 三,可变参数模板的使用
- 参数包作为整体
- 包展开
- 错误示范1(不能args[i])
- 普通一次展开
- 递归展开
- 错误示范2(if运行时判断)
- if constexpr 编译时判断解决问题
- 非递归展开
一,什么是可变参数模板
什么是可变?
可变就是可以不同。可变参数,即:参数类型可变,参数个数可变。
- C++11之前,类模板和函数模板中只能包含固定数量的模板参数。
- C++11新增了可变参数模板,让我们可以对模板的参数高度泛化,即:可以直接传一个参数包给模板参数,这个参数包可以接受任意数量和任意类型的参数。
二,基本语法
使用 typename...
或 class...
声明模板参数包,...
就代表是一个参数包:
template<typename T, typename... Args>
void myFunc(T first, Args... rest) { /*...*/ } // 使用模板的参数包
Args
是模板的类型参数包,表示接收一组类型Args... rest
是函数参数包,表示使用Args
类型,接收一组参数(后面要跟...
)。它将模板参数包展开为一组函数参数Args
只是参数包的名字,可以取其他名字- 参数包代表可接收
0
个或多个参数
三,可变参数模板的使用
参数包作为整体
我们在使用时,可以往myFunc
里传入不同个数和不同类型的参数。
编译器在编译的过程中,根据myFunc
传入的不同参数,实例化出带有不同参数的myFunc
函数
示例:
template<class ...Args>
void Myfunc(Args...args)
{cout << sizeof...(args) << endl;
}int main()
{int x1 = 1;double x2 = 2.2;std::string s1 = "hello world";Myfunc(x1); // 输出:1Myfunc(x1, x2); // 输出:2Myfunc(x1, x2, s1); // 输出:3return 0;
}
说明:
- 可以理解为,在编译的时候,编译器根据传入的不同参数的
Myfunc
实例化出了三个:Myfunc(int x1)
、Myfunc(int x1, int x2)
、Myfunc(int x1, int x2, string s1)
sizeof...(args)
:sizeof...
是一个整体运算符,用来求函数参数包中参数的个数,这里的args
就是一个整体,即:一组参数。
包展开
对于一个参数包,我们可以把他当做一个整体进行使用,如上面的sizeof...(args)
,我们就是直接把args
当一个整体进行使用。
如果我们想要拿到里面的每一个参数就需要用到包展开。直接将参数包依次展开依次作为实参给⼀个函数去处理。
【注意,参数包可不能args[i]
这样下标访问使用。因为:参数包是在编译时就确定的一组参数。在编译时,编译器会根据具体的模板实例化来处理参数包,而不像数组那样在运行时存在于内存中】
错误示范1(不能args[i])
假设你想打印出函数参数包里面的所有参数。
template<class ...Args>
void Print(Args...args)
{cout << args... << endl;
}
你这样肯定不行,会报错:“args”: 未声明的标识符。因为参数列表里面args
在编译时早就被具体实例化成了有具体类型的函数了,如Print(1.1, 2)
就被实例化成:Print(double x1, int x2)
;
普通一次展开
在使用递归展开的时候,我们先来看一个简单的展开:
template<class T1, class T2, class...Args>
void ShowList(T1 x1, T2 x2, Args...args)
{cout << x1 << endl;cout << x2 << endl;cout << sizeof...(args) << endl;
}int main()
{ShowList(1, 2.2, 3.3, "hello world");return 0;
}
输出结果:
在这里,我们传入一组参数,其中1
、2.2
分别给了x1
和x2
,后面的3.3
和"hello world"
被打包给了args
递归展开
写法三大要点:
- 递归终止函数
- 递归的包展开函数,参数列表为:(单个参数,参数包)【这样原来传入的参数包,第一个参数会被提出来,第2 - n个参数会被作为新参数包】。然后在这个函数内,就可以对这个提出来的参数进行操作
- 外壳函数,即:在递归函数外面封一层,用于把参数包传给递归包展开函数
示例:
Print
打印出函数参数包里面的所有参数
// 递归终止函数:当参数列表为空时匹配这个函数
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, string("xxxxx"), 2.2);return 0;
}
args...
:...
是包展开操作符- 在调用到
ShowList
的时候:最少传入一个参数(因为有个T x
) - 当没有参数传入的时候,就会匹配到
ShowList()
结束
运行结果:
1 xxxxx 2.2
具体编译时实例化理解图:
当然也可以编写有参的结束函数:
//递归终止函数
template<class T>
void ShowList(T x)
{cout << x << endl;
}
当ShowList
参数个数为1
的时候编译器会找最匹配的,也就是调到这个ShowList(T x)
,然后执行完以后,ShowList(T x)
里面没有再调用ShowList
于是完成终止
错误示范2(if运行时判断)
那在递归函数里面,利用sizeof...(args) == 0
来终止可以吗?
答案是不可以!!!
template<class T, class ...Args>
void ShowList(T value, Args... args)
{cout << value << " "; //打印传入的若干参数中的第一个参数if (sizeof...(args) == 0){return;}ShowList(args...); //将剩下参数继续向下传
}int main()
{ShowList(1, 2.2, 3.3, "hello world");return 0;
}
报错:“ShowList”: 未找到匹配的重载函数
为什么 if (sizeof...(args) == 0)
无法终止递归(以下内容由AI生成):
1. sizeof...(args)
是编译期常量,但 if
是运行时判断
sizeof...(args)
的值在编译时就已经确定(比如args
有 3 个参数时,sizeof...(args)
就是3
)。- 但
if (sizeof...(args) == 0)
这个if
语句本身是运行时执行的,编译器仍然会 生成所有分支的代码,包括ShowList(args...)
的调用。
2. 编译器必须实例化 ShowList(args...)
即使 if
条件在运行时是 false
,编译器仍然要 确保 ShowList(args...)
的调用是合法的,因为:
- C++ 的模板实例化规则要求 所有可能调用的模板函数都必须有定义。
- 即使
if
条件保证ShowList(args...)
不会执行,编译器仍然会 尝试实例化它。
3. 当 args...
为空时,ShowList()
无匹配版本
- 当递归到最后一步,
args...
为空时,ShowList(args...)
就变成了ShowList()
。 - 但代码 没有定义
ShowList()
这个无参数版本,我们的ShowList(T value, Args... args)
,args
可以为空,但是value
必须给。所以编译器报错:error: no matching function for call to 'ShowList()'
解决方法:
方法1 : 像之前介绍的一样,额外写第一个终止函数。
if constexpr 编译时判断解决问题
方法 2:用 if constexpr
(C++17 编译时判断)【已验证有效】
template<class T, class... Args>
void ShowList(T value, Args... args) {cout << value << " ";if constexpr (sizeof...(args) > 0) { // 编译时判断ShowList(args...); // 仅在 args 非空时实例化}// 否则自动终止
}
if constexpr
是编译期判断,不会生成无效的ShowList()
调用。
非递归展开
示例:
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)...); // 关键:展开参数包并调用 GetArg 处理每个参数
}
int main()
{Print(1, 2.2, 3.3, "hello world");return 0;
}
-
GetArg
参数包展开及使用函数GetArg(args)...
:...
让编译器遍历args
的每一个参数,并对 每一个参数 调用GetArg
,相当于:Arguments(GetArg(arg1), GetArg(arg2), ..., GetArg(argN));
GetArg
必须要有返回值,用于让Arguments
接收到参数
-
Arguments
空函数- 仅用于 接收展开后的参数包,确保
GetArg(args)...
能正确展开。 - 在 C++ 中,参数包展开
GetArg(args)...
必须发生在合法的上下文环境中,Arguments(GetArg(args)...)
提供的就是这样一个上下文。【可以理解为,就需要一个能接受参数包的容器来接收GetArg(args)...
的返回值】
- 仅用于 接收展开后的参数包,确保
🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!