[C++11]可变参数模板和参数包展开
可变参数模板
文章目录
- 可变参数模板
- 可变参数模板的概念
- 可变参数模板的定义方式
- 模板参数包的展开
- 递归展开参数包
- sizeof...计算参数包大小
- 逗号表达式展开参数包
- enable_if方式展开
- 折叠表达式展开
- 总结
可变参数模板的概念
可变参数模板(Variadic templates)是C++11新增的最强大的特性之一,它对参数高度泛化,能够让我们创建可以接受可变参数的函数模板和类模板。
- 在C++11之前,类模板和函数模板中只能包含固定数量的模板参数,可变模板参数无疑是一个巨大的改进,但由于可变参数模板比较抽象,因此使用起来需要一定的技巧。
- 在C++11之前其实也有可变参数的概念,比如printf函数就能够接收任意多个参数,但这是函数参数的可变参数,并不是模板的可变参数。
可变参数模板的定义方式
template <class... T>
void func(T... args)
{
//...
}
上面的我们把带…的模板参数称为模板参数包(template parameter pack)
上面这个函数模板的参数 args
前面有省略号,我们称之为模板参数包(template parameter pack)的可变模版参数,它里面包含了0到N个模版参数,而我们是无法直接获取 args
中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数。
模板参数包的展开
递归展开参数包
递归展开的方式如下:
- 函数模板除去参数包可变模板参数外至少要有一个模板参数,用于从参数包中拿出一个参数
- 剩余参数包递归调用
- 每次剥离一个参数,直至参数包空
template <class T, class... Args>
void foo(T first, Args... args)
{
cout << first << " ";
foo(args...);
}
//int main()
//foo("good", 2, "hello", 4, 110);
我们写出如上一个带可变模板参数的函数foo,我们尝试在main函数中调用发现无法调用
有递归自然有出口,当函数包空时,仍会递归一个空包作为参数,而我们没有空参数函数,所以我们还要再增加一个空参数的函数来进行特化
如下:
void foo()
{
cout << endl;
}
template <class T, class... Args>
void foo(T first, Args... args)
{
cout << first << " ";
foo(args...);
}
int main()
{
foo(1, 2, 3, 4);
foo("good", 2, "hello", 4, 110);
return 0;
}
//输出
//1 2 3 4
//good 2 hello 4 110
当然我们可以规定递归出口为其他数量的参数,如:
void foo(int a)
{
cout << endl;
}
template <class T, class... Args>
void foo(T first, Args... args)
{
cout << first << " ";
foo(args...);
}
int main()
{
foo(1, 2, 3, 4);
foo("good", 2, "hello", 4, 110);
return 0;
}
//输出
//1 2 3
//good 2 hello 4
当然,当我们定义递归出口为一个参数的函数时,我们调用foo必须传入不少于一个参数
sizeof…计算参数包大小
我们其实是可以计算参数包的大小的,如sizeof…(args)
void foo()
{
cout << endl;
}
template <class T, class... Args>
void foo(T first, Args... args)
{
cout << first << " " << sizeof...(args) << endl;
foo(args...);
}
int main()
{
foo(1, 2, 3, 4);
return 0;
}
//输出
//1 3
//2 2
//3 1
//4 0
那么我们是否可以通过对参数包大小的判断来结束函数递归,从而省去无参数或者少参数函数作为递归出口呢?
template <class T, class... Args>
void foo(T first, Args... args)
{
cout << first << " " << sizeof...(args) << endl;
if (!sizeof...(args))
return;
foo(args...);
}
我们发现直接报错了,也就是说这种方式不可行
- 我们在学习函数模板时知道,函数模板并不能直接调用,函数模板需要在编译时根据传入的实参类型进行推演,生成对应的函数,这个生成的函数才能够被调用。
- 而这个推演过程是在编译时进行的,我们函数体中递归调用函数,也就是说推演会不断继续下去,仍会进行参数包为空的函数推演,此时就会报错了,因为我们的函数至少要有一个参数,而又没有重载的空参数函数了
- 这里的if判断是在代码编译结束后,运行代码时才会所走的逻辑,也就是运行时逻辑,而函数模板的推演是一个编译时逻辑。
逗号表达式展开参数包
逗号表达式展开包其实是利用了C++11新特性,列表初始化。
我们列表初始化的原理就是先用列表构建initializer_list,再用initializer_list去构建我们的容器
如果我们把参数包放入初始化列表中会怎样呢?
template <class... Args>
void foo(Args... args)
{
initializer_list<int> a{args...};
for (auto x : a)
cout << x << " ";
}
//输出
//1 2 3 4
我们发现参数包放入初始化列表中,由于初始化列表从左往右执行,参数包中的参数会被逐个取出,此时由于没有递归展开,所以我们不需要再额外定义空参数的重载函数,传入参数也没有数目限制。
利用初始化列表和逗号表达式结合,我们可以如下展开参数包:
template <class... Args>
void foo(Args... args)
{
(void)initializer_list<int>{(cout << args << " ", 0)...};
}
int main()
{
foo(1, 2, 3, 4, "GenshinImpact", 3.14);
return 0;
}
//输出
//1 2 3 4 GenshinImpact 3.14
我们发现很顺利的输出了,甚至不受类型限制
其实剖析一下发现逗号表达式是一种很犯规的写法,我们逗号表达式的返回值是最右边的表达式,也就是0,所以最终用来初始化列表的元素是0,但是由于列表初始化要从左向右执行,所以我们的参数包会被展开,假如参数包有N个参数,我们展开N次,但是此次返回值都是0,所以得到了N个0的列表,而参数包内的内容都被输出了。
enable_if方式展开
enable_if是C++11新引入的一个结构体,定义如下:
// Primary template.
/// Define a member typedef @c type only if a boolean constant is true.
template<bool, typename _Tp = void>
struct enable_if
{ };
// Partial specialization for true.
template<typename _Tp>
struct enable_if<true, _Tp>
{ typedef _Tp type; };
我们可以看出下面是上面的一个偏特化。当我们传入第一个参数为true时会用第二个模板来实例化,将_Tp typedef为type,而第一个模板什么也没做。
故而enable_if常用于需要根据不同的类型的条件实例化不同模板的情形。也就是说,在不同条件下选用不同类型,其广泛的应用在 C++ 的模板元编程(meta programming)之中,利用的就是SFINAE原则,英文全称为Substitution failure is not an error,意思就是匹配失败不是错误,假如有一个特化会导致编译时错误,只要还有别的选择,那么就无视这个特化错误而去选择另外的实现。
因而我们可以借此来解决我们递归展开函数包递归出口函数和参数限制的问题。
具体流程就是:
- 利用参数包构建tuple(元组)
- 以下标访问元组元素,同时利用下标是否等于元组元素个数作为条件重载两个函数
- 当下标小于value,那么对对应下标元素操作
- 当下标等于value,则进入对应函数体
代码如下:
template <size_t k = 0, class tup>
typename enable_if<k == std::tuple_size<tup>::value>::type _foo(const tup &t)
{
cout << endl;
}
template <size_t k = 0, class tup>
typename enable_if < k<std::tuple_size<tup>::value>::type _foo(const tup &t)
{
cout << get<k>(t) << " ";
_foo<k + 1, tup>(t);
}
template <class... Args>
void foo(Args... args)
{
_foo<0>(make_tuple(args...));
}
int main()
{
foo(2023, "GenshinImpact", "hello", 2024);
return 0;
}
优雅,实在是太优雅了。
折叠表达式展开
前面几种都是C++11的内容,而我们的折叠表达式(Fold Expressions)则是我们C++17的新语法特性,使用折叠表达式可以简化对C++11中引入的参数包的处理,可以在某些情况下避免使用递归,更加方便的展开参数。
如下示例:
template <class... Args>
void foo(Args... args)
{
(cout << ... << args) << endl;
}
int main()
{
foo(2023, "GenShinImpact", "hello");
return 0;
}
简洁了不少,但是如何格式化呢?需要增加格式化辅助函数。
template <class T>
string format(const T &t)
{
stringstream ss;
ss << " " << t << " ";
return ss.str();
}
template <class... Args>
void foo(Args... args)
{
(cout << ... << format(args)) << endl;
}
int main()
{
foo(2023, "GenShinImpact", "hello");
return 0;
}
//输出
// 2023 GenShinImpact hello
也可以直接利用逗号表达式进行简化
template <class... Args>
void foo(Args... args)
{
(cout << ... << (cout << args, " ")) << endl;
}
int main()
{
foo(2023, "GenShinImpact", "hello");
return 0;
}
//输出
//2023 GenShinImpact hello
括号里逗号表达式的返回值是" “,当输出完从参数包里拆出的args,返回” "给左边的输出流输出
总结
可变参数模板参数高度泛化,提高了编程的泛用性。
而为了实现可变参数模板我们引入了参数包,于是需要对参数包进行展开,我们可以:
- 通过递归每次拆出一个参数,展开参数包
- 利用初始化列表展开参数包
- 通过enable_if和元组结合展开参数包
- C++17直接引入折叠表达式,便于展开参数包