CTTCG 04 Variadic Templates
可变长表达式可以对参数包逐个处理。与折叠表达式(相当于函数式编程中的 reduce,但如果用上逗号表达式就能表达 foreach)不同,可变长表达式(相当于函数式编程中的 map)不会改变参数数量,允许的形式也比折叠表达式宽松一点。
参数包(Parameter Pack)和可变长模板
模板函数可以指定参数包,用 sizeof…()
运算符可以获得参数包的数量。至少有一个参数包的模板称为可变长模板(variadic template)。
0 Pack Expansion:参数传递
参数传递。
Note
2024 年 2 月 20 日:除了作为函数参数之外,还能用作初始化列表(花括号)的参数。
1 递归:逐个处理
#include <iostream>
void print ()
{
}
template<typename T, typename... Types>
void print (T firstArg, Types... args)
{
std::cout << firstArg << '\n'; // print first argument
print(args...); // call print() for remaining arguments
}
还要留一个非模板的单参数或者零参数的函数收尾。如果收尾的函数是零参数的,可以使用 if constexpr
判断剩余参数数量,从而避免多写一个函数。
2 折叠表达式:一次性处理
Note
warning: fold-expressions only available with ‘-std=c++17’ or ‘-std=gnu++17’ (-Wc++17-extensions)
这是折叠表达式的一个例子:
template<typename... T>
auto foldSum (T... s) {
return (... + s); // ((s1 + s2) + s3) ...
}
折叠表达式可以将参数包变成一个表达式,一下子利用完了所有参数,而递归函数模板是把参数数量逐次减 1,直到计算完成。
注意折叠表达式的形式有限制:
省略号、op、pack 三者紧紧连接,省略号的位置就是优先结合的位置,表达式两端一定要加上括号。
Tip
怎么记忆结合顺序呢?在有 init 的时候,靠着 init 所在的方向结合。在没有 init 的时候,靠着 ...
所在的方向结合。
折叠表达式在形式上有限制,因此我们不能给下面的函数模板添加参数间补充空格的功能:
template<typename... Types>
void print (Types const&... args)
{
(std::cout << ... << args) << '\n'; // 能正常编译运行,但是想要在每个参数后加空格就做不到
}
这个追加空格的功能还可以用之后提到的可变长表达式实现。
示例:验证 ...
的位置会影响参数的结合顺序
以下代码中 sum
中的两行输出结果不同:
#include <cstdio>
struct Integer {
int x;
Integer(int x) : x{x} {}
friend Integer operator+(Integer a, Integer b) {
Integer c{a.x + b.x};
printf("%d+%d=%d\n", a.x, b.x, c.x);
return c;
}
};
template <typename... Ts>
Integer sum(Ts... ts) {
// return (0 + ... + ts); // #1
return (ts + ... + 0); // #2
}
int main() {
sum(Integer{2}, Integer{4}, Integer{8});
}
3 可变长表达式(Variadic Expression):参数传递
可变长表达式可以对参数包逐个处理。与折叠表达式(相当于函数式编程中的 reduce,但如果用上逗号表达式就能表达 foreach)不同,可变长表达式(相当于函数式编程中的 map)不会改变参数数量,允许的形式也比折叠表达式宽松一点。
template<typename... T>
void addOne (T const&... args)
{
// print (args + 1...); // ERROR: 1... is a literal with too many decimal points
// print (args + 1 ...); // OK
print ((args + 1)...); // OK,个人觉得这种多加括号的行为更加保守一点
}
一个参数包用可变长表达式变形之后,仍然可以交给折叠表达式处理:
#include <iostream>
template <typename... Ts>
void print(Ts... nums) {
(..., (std::cout << nums << ' ')); // 同时用到了可变长表达式和折叠表达式
std::cout << '\n';
}
int main() {
print(1, 34, 7);
}
以上 print
函数中省略号无论是在左侧还是在右侧都不影响打印数字的顺序,这并不是它不遵守折叠表达式的结合律,而是因为逗号运算符两边的参数谁加括号都不影响真正的执行顺序。
可变长基类
C++17 允许模板类继承多个基类:
template<typename... Bases>
struct Overloader : Bases...
{
using Bases::operator()...; // OK since C++17
};
这是 cppreference 上面可以见到的一种访问 std::variant
前,将 lambda 表达式装入 visitor 的写法。
可变长模板的应用
a) 可变长表达式可以用于先用下标访问元素,再将结果交给参数包函数模板打印。
b) 通过给类增加参数包,可以实现 std::variant
。
c) 可以用于 deduction guide:
namespace std {
template<typename T, typename... U> array(T, U...)
-> array<enable_if_t<(is_same_v<T, U> && ...), T>,
(1 + sizeof...(U))>;
}
这个推导指引检查了初始化列表是否为同类型参数,使得 std::array
在某些场合可以省略参数列表:std::array a{42,45,77};