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”);
文章目录