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

📝前言:

这篇文章我们来讲讲C++11------可变参数模板:

🎬个人简介:努力学习ing

📋个人专栏:C++学习笔记

🎀CSDN主页 愚润求学

🌄其他专栏:C语言入门基础python入门基础python刷题专栏Linux


文章目录

一,什么是可变参数模板

什么是可变?

可变就是可以不同。可变参数,即:参数类型可变,参数个数可变。

  • C++11之前,类模板和函数模板中只能包含固定数量的模板参数
  • C++11新增了可变参数模板,让我们可以对模板的参数高度泛化,即:可以直接传一个参数包给模板参数,这个参数包可以接受任意数量和任意类型的参数。

二,基本语法

使用 typename...class... 声明模板参数包,...就代表是一个参数包:

cpp 复制代码
template<typename T, typename... Args>
void myFunc(T first, Args... rest) { /*...*/ } // 使用模板的参数包
  • Args 是模板的类型参数包,表示接收一组类型
  • Args... rest 是函数参数包,表示使用Args类型,接收一组参数(后面要跟...)。它将模板参数包展开为一组函数参数
  • Args只是参数包的名字,可以取其他名字
  • 参数包代表可接收0个或多个参数

三,可变参数模板的使用

参数包作为整体

我们在使用时,可以往myFunc里传入不同个数和不同类型的参数。

编译器在编译的过程中,根据myFunc传入的不同参数,实例化出带有不同参数的myFunc函数

示例:

cpp 复制代码
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);  // 输出:1
	Myfunc(x1, x2);  // 输出:2
	Myfunc(x1, x2, s1);  // 输出:3
	return 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])

假设你想打印出函数参数包里面的所有参数。

cpp 复制代码
template<class ...Args>
void Print(Args...args)
{
	cout << args... << endl;
}

你这样肯定不行,会报错:"args": 未声明的标识符。因为参数列表里面args在编译时早就被具体实例化成了有具体类型的函数了,如Print(1.1, 2)就被实例化成:Print(double x1, int x2);


普通一次展开

在使用递归展开的时候,我们先来看一个简单的展开:

cpp 复制代码
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;
}

输出结果:

在这里,我们传入一组参数,其中12.2分别给了x1x2,后面的3.3"hello world"被打包给了args


递归展开

写法三大要点:

  • 递归终止函数
  • 递归的包展开函数,参数列表为:(单个参数,参数包)【这样原来传入的参数包,第一个参数会被提出来,第2 - n个参数会被作为新参数包】。然后在这个函数内,就可以对这个提出来的参数进行操作
  • 外壳函数,即:在递归函数外面封一层,用于把参数包传给递归包展开函数

示例:
Print 打印出函数参数包里面的所有参数

cpp 复制代码
// 递归终止函数:当参数列表为空时匹配这个函数
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

具体编译时实例化理解图:

当然也可以编写有参的结束函数:

cpp 复制代码
//递归终止函数
template<class T>
void ShowList(T x)
{
	cout << x << endl;
}

ShowList参数个数为1的时候编译器会找最匹配的,也就是调到这个ShowList(T x),然后执行完以后,ShowList(T x)里面没有再调用ShowList于是完成终止


错误示范2(if运行时判断)

那在递归函数里面,利用sizeof...(args) == 0来终止可以吗?

答案是不可以!!!

cpp 复制代码
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 编译时判断)【已验证有效】

cpp 复制代码
template<class T, class... Args>
void ShowList(T value, Args... args) {
    cout << value << " ";
    if constexpr (sizeof...(args) > 0) {  // 编译时判断
        ShowList(args...);               // 仅在 args 非空时实例化
    }
    // 否则自动终止
}
  • if constexpr 是编译期判断,不会生成无效的 ShowList() 调用。

非递归展开

示例:

cpp 复制代码
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)...的返回值】

🌈我的分享也就到此结束啦🌈

要是我的分享也能对你的学习起到帮助,那简直是太酷啦!

若有不足,还请大家多多指正,我们一起学习交流!

📢公主,王子:点赞👍→收藏⭐→关注🔍

感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

相关推荐
freexyn7 分钟前
Matlab自学笔记五十二:变量名称:检查变量名称是否存在或是否与关键字冲突
人工智能·笔记·算法·matlab
*TQK*14 分钟前
线性代数——行列式⭐
笔记·学习·线性代数
YuforiaCode15 分钟前
第十二届蓝桥杯 2021 C/C++组 空间
c语言·c++·蓝桥杯
兜小糖的小秃毛18 分钟前
文号验证-同时对两个输入框验证
开发语言·前端·javascript
YuforiaCode22 分钟前
第十二届蓝桥杯 2021 C/C++组 卡片
c语言·c++·蓝桥杯
anqi2744 分钟前
如何在 IntelliJ IDEA 中编写 Speak 程序
java·大数据·开发语言·spark·intellij-idea
XuX031 小时前
MATLAB小试牛刀系列(1)
开发语言·matlab
Suckerbin1 小时前
第十四章-PHP与HTTP协议
开发语言·http·php
Best_Liu~1 小时前
TransactionTemplate 与@Transactional 注解的使用
java·开发语言·spring boot·后端
谈不譚网安1 小时前
初识Python
开发语言·python