「C++ 进阶语法」可变参数模板与参数包展开输出
文章大图来源:pixiv_id=65463852
1 可变参数模板
1.1 基本概念
可变参数模板(variadic templates)是 C++ 11 引入的一个强大的特性。它允许模板函数或模板类接受可变数量的参数。这在编写泛型代码时非常有用,例如实现像 printf 这样可以接受不同数量和类型参数的函数。
1.2 基本语法
可变参数模板使用省略号 ... 来表示 参数包(parameter pack)。参数包可以包含零个或多个模板参数:
1 | |
在上面的示例中,typename... 表明当前声明的是一个可变参数模板。Args 是一个模板参数包。args 是 Args... 类型的参数,也就是函数 work 的参数包(参数列表)。
Args 可以包含任意数量(包括零个)的不同类型的模板参数,args 则包含了实际传递给函数的参数。
例如下面的示例中,将 args 参数包通过 args... (在后面加上省略号 ...)展开,并存储到一个 std::tuple 元组中。
1 | |
2 参数包展开输出方法
2.1 递归方式展开
假如我们要实现一个 print 函数,用于打印可变数量的参数。参数包的每个参数依次打印,并且用空格分隔,最终末尾输出回车符。
我们可以通过 可变参数模板 结合 递归 来实现。基本思路为:
- 定义一个递归终止函数,当参数包只有一个参数时调用,以停止递归。末尾输出回车符
- 定义一个递归函数,每次输出参数包的第一个参数。每个参数输出时后面加上空格符。
由此,可以得到如下实现:
1 | |
在上面的代码中,print(T t) 是基础模板,同时也是递归终止函数。当只有一个参数时,后面输出回车符。
print(First first, Rest... rest) 是递归调用函数,首先打印第一个参数 first,然后通过 print(rest...) 调用自身来处理剩余参数。rest... 也就是展开剩余的参数 rest,作为新的参数列表并传递给下一层递归时的 print 函数调用。
2.2 逗号表达式展开
如何避免采用递归的方式实现展开参数包输出参数?以下是一个简单的实现代码:
1 | |
在上面的代码中,定义了一个数组 arr。通过类似于展开参数列表 args... 的方式,通过 (cout << args << ' ', 0)... 的方式来展开并输出。
(cout << args << " ", 0) 是一个 逗号表达式,当执行这个表达式时,会先执行 cout << args << ' ' 这一句,然后执行 0。而逗号表达式最终的值为最后一个逗号后面的子表达式的值,此处也就是 0。
(cout << args << " ", 0) 后面加上省略号 ... 来展开参数包,对于参数包args 中的每一个元素,都会执行一次逗号表达式 (std::cout << args << " ", 0),也就是依次将每个参数输出。
而 arr 数组存储的元素全都是 0,因此这里的数组并没有什么实际作用,只是通过其初始化列表的语法特性来协助展开参数包并输出。
当然,我们也可以使用一种轻量级的容器类型 std::initializer_list 来代替数组 arr:
1 | |
2.3 折叠表达式(C++ 17)
-
折叠表达式概念和语法
折叠表达式(fold expressions)是 C++ 17 引入的一个新特性,主要用于简化可变参数模板中对参数包的操作。它提供了一种简洁的方式来对参数包中的所有元素进行二元运算。例如,在处理像求和、求积等操作时,折叠表达式可以避免复杂的递归或迭代过程。
基本的语法形式为
(pack op...)或者(... op pack),其中pack是参数包,op是一个二元运算符。例如
(args + ...)就是一个折叠表达式,其中args是一个参数包,+是加法运算符。这个表达式会将参数包args中的所有元素相加。 -
折叠方向
-
左折叠
语法形式为
(pack op...)。它从 左到右 依次应用二元运算符。例如,对于参数包a, b, c和运算符+,左折叠表达式(a + b + c)等价于((a + b) + c)。比如下面的示例:1
2
3
4
5
6
7
8
9
10
11
12#include <iostream>
template<typename... Args>
auto sum_left(Args... args) {
return (args + ...);
}
int main() {
std::cout << sum_left(1, 2, 3) << std::endl;
// 输出6,计算过程为((1 + 2)+3)
return 0;
} -
右折叠
语法形式为
(... op pack)。它从 右到左 依次应用二元运算符。对于参数包a, b, c和运算符+,右折叠表达式(... + args)等价于(a + (b + c))。比如下面的示例:1
2
3
4
5
6
7
8
9
10
11
12#include <iostream>
template<typename... Args>
auto sum_left(Args... args) {
return (... + args);
}
int main() {
std::cout << sum_left(2, 3, 4) << std::endl;
// 输出9,计算过程为(2 + (3 + 4))
return 0;
}
-
以上是折叠表达式的基本概念和语法。
我们可以通过 折叠表达式 来实现可变参数模板的参数包展开和参数打印。由上面的语法,我们可以采用左折叠的形式,将 (cout << args << " ") 作为参数包,, 逗号作为运算符。这样我们可以通过 ((cout << args << ' '), ...) 的方式来展开参数包并依次输出,相当于执行了一个有若干个输出的逗号表达式。
以下是代码实现:
1 | |