c++ 参数包
基本用法
template<typename T>
T adder(T v) {
return v;
}
template<typename T, typename... Args>
T adder(T first, Args... args) {
return first + adder(args...);
}
long sum = adder(1, 2, 3, 8, 7);
std::string s1 = "x", s2 = "aa", s3 = "bb", s4 = "yy";
std::string ssum = adder(s1, s2, s3, s4);
typename ... Args被称为模板参数包,而Args ... args被称为函数参数包(Args当然是完全任意的名称,可以是其他任何名称)。
可变参数模板的编写方式与您编写递归代码的方式相同-您需要一个基本情况(上面的adder(T v)声明)和一个“递归”的一般情况[1]。
递归本身发生在调用加法器(args ...)中。
请注意如何定义通用加法器-将第一个参数从模板参数包中剥离为T类型(因此,参数优先)。
因此,每次调用时,参数包都会缩短一个参数。 最终,遇到了基本情况。
处理最后的参数包,可以写一个处理最后一个的函数重载定义,也可以写一个处理无参数的函数重载。如:
#include <bits/stdc++.h>
using namespace std;
//1
int add()
{
cout<<"first"<<endl;
return 0;
}
//2
int add(int a)
{
cout<<"second"<<endl;
return a;
}
template<class... Targs>
int add(int a, Targs... args)
{
int b = add(args...);
return a+b;
}
void test2()
{
int ans = add(1, 2, 3, 4);
cout<<ans<<endl;
}
int main()
{
test2();
return 0;
}
当不存在第二个int add(int a)的函数定义的时候,会调用第一个int add(),否则会选择更“合适”的第二个定义.
简单变体
template<typename T>
bool pair_comparer(T a, T b) {
// In real-world code, we wouldn't compare floating point values like
// this. It would make sense to specialize this function for floating
// point types to use approximate comparison.
return a == b;
}
template<typename T, typename... Args>
bool pair_comparer(T a, T b, Args... args) {
return a == b && pair_comparer(args...);
}
pair_comparer接受任意数量的参数,并且仅当它们成对相等时才返回true。 类型不强制执行-可以比较的所有内容都可以使用。 例如:
pair_comparer(1.5, 1.5, 2, 2, 6, 6)
返回true。 但是,如果我们将第二个参数更改为仅1,则由于double和int不是同一类型,因此无法编译。(这家伙在编译时检查错误。。。。)
更有趣的是,pair_comparer仅适用于偶数个参数,因为它们是成对剥离的,并且基本情况将两个参数进行比较。 下列:
pair_comparer(1.5, 1.5, 2, 2, 6, 6, 7)
不编译; 编译器抱怨基本情况需要2个参数,但只提供1个。 为了解决这个问题,我们可以添加功能模板的另一种形式:
template<typename T>
bool pair_comparer(T a) {
return false;
}
在这里,我们强制所有奇数个参数序列返回false,因为当只剩下一个参数时,该版本会被匹配。
请注意,pair_comparer强制比较对中的两个成员都具有完全相同的类型。 一个简单的变化就是允许不同的类型,只要它们可以被比较即可。 我将把这个练习留给有兴趣的读者。
性能
如果您担心依赖可变参数模板的代码的性能,请不必担心。 由于不涉及实际的递归,因此我们所拥有的只是在编译时预先生成的一系列函数调用。 实际上,该序列很短(很少有5-6个以上的自变量调用)。 由于现代编译器积极地内联代码,因此最终可能会被编译为绝对没有函数调用的机器代码。 实际上,最终得到的结果与循环展开没有什么不同。
与C样式可变参数相比,这是一个明显的胜利,因为C样式可变参数必须在运行时解析。 va_宏实际上是在操纵运行时堆栈。 因此,可变参数模板通常是可变参数功能的性能优化。
可变参数数据结构
让我们从类型定义开始:
template <class... Ts> struct tuple {};
template <class T, class... Ts>
struct tuple<T, Ts...> : tuple<Ts...> {
tuple(T t, Ts... ts) : tuple<Ts...>(ts...), tail(t) {}
T tail;
};
我们从基本情况开始-一个名为tuple的类模板的定义,该模板为空。
随后的专业化从参数包中剥离了第一种类型,并定义了该类型的名为tail的成员。它也源自与其余包实例化的元组。
这是一个递归定义,当没有更多类型要剥离并且层次结构的基础是空元组时,该定义将停止。 为了更好地了解生成的数据结构,让我们使用一个具体的示例:
tuple<double, uint64_t, const char*> t1(12.2, 42, "big");
因此,上面的struct定义使我们可以创建元组,但是我们还不能做其他很多事情。
首先,我们必须定义一个帮助程序类型,以便我们可以访问元组中第k个元素:
template <size_t, class> struct elem_type_holder;
template <class T, class... Ts>
struct elem_type_holder<0, tuple<T, Ts...>> {
typedef T type;
};
template <size_t k, class T, class... Ts>
struct elem_type_holder<k, tuple<T, Ts...>> {
typedef typename elem_type_holder<k - 1, tuple<Ts...>>::type type;
};
elem_type_holder是另一个可变参数类模板。 它以数字k和我们感兴趣的元组类型作为模板参数。
template <size_t k, class... Ts>
typename std::enable_if<k == 0, typename elem_type_holder<0, tuple<Ts...>>::type&>::type
get(tuple<Ts...>& t) {
return t.tail;
}
template <size_t k, class T, class... Ts>
typename std::enable_if<k != 0, typename elem_type_holder<k, tuple<T, Ts...>>::type&>::type
get(tuple<T, Ts...>& t) {
tuple<Ts...>& base = t;
return get<k - 1>(base);
}
在这里,enable_if用于在get的两个模板重载之间进行选择-一个用于k为零时的重载,一个用于一般情况下的重载,该重载会剥离第一类型并递归,这与可变函数模板一样。
由于它返回一个引用,因此我们可以使用get来读取和写入元组元素:
#include <bits/stdc++.h>
template <class... Ts> struct tuple {};
template <class T, class... Ts>
struct tuple<T, Ts...> : tuple<Ts...> {
tuple(T t, Ts... ts) : tuple<Ts...>(ts...), tail(t) {}
T tail;
};
template <size_t, class> struct elem_type_holder;
template <class T, class... Ts>
struct elem_type_holder<0, tuple<T, Ts...>> {
typedef T type;
};
template <size_t k, class T, class... Ts>
struct elem_type_holder<k, tuple<T, Ts...>> {
typedef typename elem_type_holder<k - 1, tuple<Ts...>>::type type;
};
template <size_t k, class... Ts>
typename std::enable_if<k == 0, typename elem_type_holder<0, tuple<Ts...>>::type&>::type
get(tuple<Ts...>& t) {
return t.tail;
}
template <size_t k, class T, class... Ts>
typename std::enable_if<k != 0, typename elem_type_holder<k, tuple<T, Ts...>>::type&>::type
get(tuple<T, Ts...>& t) {
tuple<Ts...>& base = t;
return get<k - 1>(base);
}
int main()
{
tuple<double, uint64_t, const char*> t1(12.2, 42, "big");
std::cout << "0th elem is " << get<0>(t1) << "\n";
std::cout << "1th elem is " << get<1>(t1) << "\n";
std::cout << "2th elem is " << get<2>(t1) << "\n";
get<1>(t1) = 103;
std::cout << "1th elem is " << get<1>(t1) << "\n";
return 0;
}
参数包的两种展开方式
方式一 递归展开
#include <iostream>
using namespace std;
//递归终止函数
void print()
{
cout << "empty" << endl;
}
//展开函数
template <class T, class ...Args>
void print(T head, Args... rest)
{
cout << "parameter " << head << endl;
print(rest...);
}
int main(void)
{
print(1,2,3,4);
return 0;
}
2 逗号表达式展开
template <class T>
void printarg(T t)
{
cout << t << endl;
}
template <class ...Args>
void expand(Args... args)
{
int arr[] = {(printarg(args), 0)...};
}
expand(1,2,3,4);
expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。
同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
我们可以把上面的例子再进一步改进一下,将函数作为参数,就可以支持lambda表达式了,从而可以少写一个递归终止函数了,具体代码如下:
template<class F, class... Args>void expand(const F& f, Args&&...args)
{
//这里用到了完美转发,关于完美转发,读者可以参考笔者在上一期程序员中的文章《通过4行代码看右值引用》
initializer_list<int>{(f(std::forward< Args>(args)),0)...};
}
expand([](int i){cout<<i<<endl;}, 1,2,3);
上面的例子将打印出每个参数,这里如果再使用C++14的新特性泛型lambda表达式的话,可以写更泛化的lambda表达式了:
expand([](auto i){cout<<i<<endl;}, 1,2.0,”test”);