文章目录
- 1. 认识可变参数模板
- 2. 可变参数模板的定义方式
- 3. 参数包的展开方式
- 3.1 递归展开参数包
- 3.2 逗号表达式展开参数包
1. 认识可变参数模板
可变参数模板是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接收可变参数的函数模板和类模板。
- 在C++11之前,类模型和函数模型中只能包含固定数量的模板参数,可变参数模板无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定技巧。
- 在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。
2. 可变参数模板的定义方式
- 模板参数Args前面有省略号,代表它是一个可变参数模板,我们把带省略号的参数称为参数包,参数包里面可以包含0到N(N>=0)个模板参数,而args则是一个函数形参参数包。
- 模板参数包Args和函数形参参数包args的名字可以任意指定。
现在调用ShowList函数时就可以传入任意多个参数了,并且这些参数可以是不同类型的。
我们可以在函数模板中通过sizeof计算参数包中参数的个数
但是我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式获取,这是使用可变参数模板的一个主要特点,也是最大的难点。
3. 参数包的展开方式
3.1 递归展开参数包
递归展开参数包的方式如下:
- 给函数模板增加一个模板参数,这样就可以从接收到的参数包分离出一个参数出来。
- 在函数模板中递归调用该函数模板,调用时传入剩下的参数包。
- 如此递归下去,每次分离出参数包的一个参数,直到参数包中的所有参数都被取出来。
比如我们要打印调用函数时传入的各个参数,那么函数模板可以这样编写:
这时我们面临的问题就是,如果终止函数的递归调用。
编写无参的递归终止函数
我们可以在刚才的基础上,再编写一个无参的递归终止函数,该函数的函数名与展开函数的函数名相同。
这样一来,当递归调用ShowList函数模板时,如果传入的参数包中参数的个数为0,那么就会匹配到这个无参的递归终止函数,这样就结束了递归。
- 但如果外部调用ShowList函数时就没有传入参数,那么就会直接匹配到无参的递归终止函数。
- 而我们本意是想让外部调用ShowList函数时匹配的都是函数模板,并不是让外部调用时直接匹配到这个递归终止函数。
鉴于此,我们可以将展开函数和递归调用函数的函数名改为ShowListArg,然后重新编写一个ShowList函数模板,该函数模板的函数体重要做的技术调用ShowListArg函数展开参数包。
这时无论外部调用时传入多少个参数,最终匹配到的都是同一个函数了。
编写带参的递归终止函数
除了编写无参的递归终止函数,也可以编写带参的递归终止函数来终止递归,比如这里编写一个带一个参数的递归终止函数。
这样一来,在递归调用过程中,如果传入的参数包中参数的个数为1,那么就会匹配到这个递归终止函数,这样也就结束了递归。但是需要注意,这里的递归调用函数需要写成函数模板,因为我们并不知道最后一个参数是什么类型的。
但该方法有一个弊端就是,我们在调用ShowList函数时必须至少传入一个参数,否则就会报错。因为此时无论是调用递归终止函数还是展开函数,都需要至少传入一个参数。
不可以通过判断参数包中参数的个数来终止递归!
既然我们可以通过sizeof计算出参数包中参数的个数,那我们能不能在ShowList函数中设置一个判断,当参数包中参数为0时就终止递归呢?
这种方法是不可行的,原因如下:
- 函数模板并不能调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
- 而这个推演过程是在编译时进行的,当推演到参数包args中参数个数为0时,还需要将当前函数推演完毕,这时就会继续推演传入0个参数时的ShowList函数,此时就会产生报错,因为ShowList函数至少要求传入一个参数。
3.2 逗号表达式展开参数包
通过列表获取参数包中的参数
数组可以通过列表进行初始化
如果参数包中各个参数的类型都是整型,那么也可以把这个参数包放到列表当初始化这个整形数组,此时参数包中参数就放到数组中了。
这时调用ShowList函数时就可以传入多个整型参数了。
但C++并不像Python这样的语言,C++规定一个容器中存储的数据类型必须是相同的,因此如果这样写的话,那么调用ShowList函数时传入的参数只能是整型的,并且还不能传入0个参数,因为数组的大小不能为0,因此我们还需要在此基础上借助逗号表达式来展开参数包。
通过逗号表达式展开参数包
虽然我们不能使用不同类型的参数去初始化一个整形数组,但我们可以借助逗号表达式。
- 逗号表达式会从左到右依次计算各个表达式,并且将最后一个表达式的值作为返回值进行返回。
- 将逗号表达式的最后一个表达式设置为一个整型值,确保逗号表达式返回的是一个整形值。
- 将处理参数包中参数的动作封装成一个函数,将该函数的调用动作作为逗号表达式的第一个表达式。
这样一来,在执行逗号表达式时就会先调用处理函数处理对应的参数,然后再将逗号表达式中的最后一个整形值作为返回值来初始化整型数组。
- 我们这里要做的就是打印参数包中的各个参数,因此处理函数中要做的就是将传入的参数进行打印即可。
- 可变参数的省略号在逗号表达式外面,表示需要将逗号表达式展开,如果省略号加在args的后面,那么参数包展开后会全部传入PrintArg函数,代码中的 {(PrintArg(args), 0)…} 将会展开成 {(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), etc…} 。
这时调用ShowList函数时就可以传入多个不同类型的参数了,但调用时仍然不能传入0个参数,因为数组的大小不能为0,如果想要支持传入0个参数,也可以写一个无参的ShowList函数。
实际上我们也可以不用逗号表达式,因为这里的问题就是初始化整型数组时必须用整数,那我们可以将处理函数的返回值设置为整型,然后用这个返回值去初始化整型数组也是可以的。