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};