「C++ 进阶语法」可变参数模板与参数包展开输出

文章大图来源:pixiv_id=65463852

1 可变参数模板

1.1 基本概念

可变参数模板(variadic templates)是 C++ 11 引入的一个强大的特性。它允许模板函数或模板类接受可变数量的参数。这在编写泛型代码时非常有用,例如实现像 printf 这样可以接受不同数量和类型参数的函数。

1.2 基本语法

可变参数模板使用省略号 ... 来表示 参数包(parameter pack)。参数包可以包含零个或多个模板参数:

1
2
template<typename... Args>
void work(Args... args);

在上面的示例中,typename... 表明当前声明的是一个可变参数模板。Args 是一个模板参数包。argsArgs... 类型的参数,也就是函数 work 的参数包(参数列表)。

Args 可以包含任意数量(包括零个)的不同类型的模板参数,args 则包含了实际传递给函数的参数。

例如下面的示例中,将 args 参数包通过 args... (在后面加上省略号 ...)展开,并存储到一个 std::tuple 元组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <bits/stdc++.h>
using namespace std;

#define ios ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)

// 将参数包展开并存储到 std::tuple 中
template<typename... Args>
std::tuple<Args...> my_make_tuple(Args... args) {
return std::tuple<Args...>{ args... }; // args... 展开参数包
}

void marisa(){
auto tup = my_make_tuple(520, "Marisa", 0.1);
cout << get<0>(tup) << "\n";
cout << get<1>(tup) << "\n";
cout << get<2>(tup) << "\n";
}

int main(){
ios;

int T = 1; // cin >> T;
while(T -- ) marisa();

return 0;
}

2 参数包展开输出方法

2.1 递归方式展开

假如我们要实现一个 print 函数,用于打印可变数量的参数。参数包的每个参数依次打印,并且用空格分隔,最终末尾输出回车符。

我们可以通过 可变参数模板 结合 递归 来实现。基本思路为:

  • 定义一个递归终止函数,当参数包只有一个参数时调用,以停止递归。末尾输出回车符
  • 定义一个递归函数,每次输出参数包的第一个参数。每个参数输出时后面加上空格符。

由此,可以得到如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <bits/stdc++.h>
using namespace std;

// 1. 递归方式展开
// 基础模板,停止递归
template<typename T>
void print(T t){
std::cout << t << "\n";
}

// 可变参数模板,每次输出一个
template<typename First, typename... Rest>
void print(First first, Rest... rest){
std::cout << first << " ";
print(rest...); // 递归调用
}

int main(){
print(1, 2, 3, "Marisa", "Alice");

return 0;
}

在上面的代码中,print(T t) 是基础模板,同时也是递归终止函数。当只有一个参数时,后面输出回车符。

print(First first, Rest... rest) 是递归调用函数,首先打印第一个参数 first,然后通过 print(rest...) 调用自身来处理剩余参数。rest... 也就是展开剩余的参数 rest,作为新的参数列表并传递给下一层递归时的 print 函数调用。

2.2 逗号表达式展开

如何避免采用递归的方式实现展开参数包输出参数?以下是一个简单的实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <bits/stdc++.h>
using namespace std;

// 2. 逗号表达式展开
template<typename... Args>
void print(Args... args) {
int arr[] = { (cout << args << " ", 0)... };
cout << "\n";
}

int main(){
print(520, 1314, "Marisa", "Alice");

return 0;
}

在上面的代码中,定义了一个数组 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
4
5
6
7
8
9
10
11
12
13
14
#include <bits/stdc++.h>
using namespace std;

template<typename... Args>
void print(Args... args){
initializer_list<bool>{ (cout << args << " ", false)... };
cout << "\n";
}

int main(){
print(520, 1314, "Marisa", "Alice");

return 0;
}

2.3 折叠表达式(C++ 17)

  1. 折叠表达式概念和语法

    折叠表达式(fold expressions)是 C++ 17 引入的一个新特性,主要用于简化可变参数模板中对参数包的操作。它提供了一种简洁的方式来对参数包中的所有元素进行二元运算。例如,在处理像求和、求积等操作时,折叠表达式可以避免复杂的递归或迭代过程。

    基本的语法形式为 (pack op...) 或者 (... op pack),其中 pack 是参数包,op 是一个二元运算符。

    例如 (args + ...) 就是一个折叠表达式,其中 args 是一个参数包,+ 是加法运算符。这个表达式会将参数包 args 中的所有元素相加。

  2. 折叠方向

    • 左折叠

      语法形式为 (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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <bits/stdc++.h>
using namespace std;

// 3. 折叠表达式
template<typename... Args>
void print(Args... args){
// 左折叠,空格分隔,逗号运算符。最后输出回车
((std::cout << args << ' '), ...) << '\n';
}

int main(){
print(1314, 521, "Marisa", "Alice");

return 0;
}

参考

  1. 可变参数

  2. 折叠表达式 (自 C++17 起)


「C++ 进阶语法」可变参数模板与参数包展开输出
https://marisamagic.github.io/2025/01/11/20250111/
作者
MarisaMagic
发布于
2025年1月11日
许可协议