模板参数遍历 | C & C++

本文介绍了使用 C++ 模板进行参数遍历的两种方式。

普通写法

1
2
3
4
5
6
7
void expand() {}

template <typename T, typename... Args>
void expand(T arg, Args... rest) {
std::cout << typeid(arg).name() << std::endl;
expand(rest...);
}

这种写法比较容易理解:通过递归的方式展开所有参数。当函数参数不为空时,会匹配到第二个参数,当函数参数为空时,会匹配到第一个参数。

封装写法

下面看一种抽象得比较好的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if_t<I == sizeof...(Tp)> expand(
std::tuple<Tp...> &t, FuncT f) {}

template <size_t I = 0, typename FuncT, typename... Tp>
inline typename std::enable_if_t <
I<sizeof...(Tp)> expand(std::tuple<Tp...> &t, FuncT f) {
f(std::get<I>(t));
expand<I + 1, FuncT, Tp...>(t, f);
}

auto args_tup = std::forward_as_tuple(args...);
expand(args_tup, [](auto arg) {
std::cout << typeid(arg).name() << std::endl;
});

这种写法把对每个参数的处理单独抽象成一个参数,只需传入一个函数作为参数,这个函数就是要对每个参数进行的处理。

这种写法看起来有点复杂,其实原理和前面那种是一样的,也是通过递归的方式对所有参数依次处理。这里涉及到了一个编译期函数 std::enable_if_t,先来解释下这个函数的作用,还是先看示例:

1
2
3
4
5
6
7
8
9
10
11
// 如果 T 的类型是 int,则定义函数 int read(void* = nullptr);否则不定义该函数
template <typename T>
T read(typename std::enable_if_t<std::is_same_v<T, int> > * = nullptr) {
return 42;
}

// 如果 T 的类型是 double,则定义函数 double read();否则不定义该函数
template <typename T>
typename std::enable_if_t<std::is_same_v<T, double>, T> read() {
return 3.14;
}

可以看到如果函数 std::enable_if_t 的模板参数只有一个,则作为定义该函数与否的判断;如果模板参数有两个,则成功匹配后整个函数等价于第二个模板参数。

了解了函数 std::enable_if_t 的用法后,再分析上述模板参数遍历的实现原理:

  1. 把可变模板参数转成一个 tuple,对该 tuple 进行递归
  2. 可以看出两个函数中的 std::enable_if_t 不会同时成立,因为 sizeof...(Tp) 的值是固定的
  3. 在第一次匹配时,如果 tuple 的大小不为 0,则第二个函数的 std::enable_if_t 的模板参数为 true,即匹配到第二个函数,取到模板参数的第一个放到函数 f 中执行
  4. 在整个匹配过程中,I 的取值为 0、1、2、…、n(n = sizeof…(Tp)),函数的匹配情况会根据 std::enable_if_t 的模板参数是否为 true 而来,即 I 小于 n 时,匹配第二个函数,I 等于 n 时,匹配第一个函数(作为递归的终点),最终完成对所有函数的处理

参考

C++ 遍历可变模板参数 iterate variadic template arguments

C++ SFINAE简介和std::enable_if_t的简单使用

作者

zhongtian

发布于

2022-08-06

更新于

2023-12-16

许可协议

评论