文章目录

  • 1.概述
  • 2.可变模版参数的展开
    • 2.1变参函数模版
      • 2.1.1递归函数方式展开参数包
      • 2.1.2逗号表达式展开参数包
    • 2.2变参类模版
      • 2.2.1偏特化与递归方式展开
      • 2.2.2继承方式展开
  • 3.变参模板的应用
    • 3.1消除重复代码
    • 3.2实现泛化的delegate
  • 4.总结
  • 参考文献

1.概述

变参模板(variadic template)是C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,掌握也存在一定的难度。

2.可变模版参数的展开

可变模板参数和普通模板参数的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename或class后面带上省略号“…”。可变参数模版的定义形式如下:

//可变参数函数模板
template<typename... T> void f(T... args);
//可变参数类模板
template<typename... T> class ClassFoo;

上面的参数中,T为模板参数包(template parameter pack),args为函数参数包(function parameter pack),参数包里面包含了0到N(N>=0)个参数。我们无法直接获取参数包中的每个参数,只能通过展开参数包的方式,这是使用可变参数模版的一个主要特点,也是最大的难点。

可变模版参数和普通模版参数语义是一致的,可以应用于函数模板和类模板,然而,可变参数函数模版和可变参数类模版展开参数包的方法有相似也有不同之处,下面我们来分别看看他们参数包展开的方法。

2.1变参函数模版

一个简单的变参函数模板。

template <class... T> void f(T... args)
{cout << sizeof...(T) <<" "<< sizeof...(args) << endl; //打印函数参数包中参数个数
}f();               //0 0
f(1, 1.2);      //2 2
f(1, 2.3, "");    //3 3

sizeof…运算符的作用是计算参数包中的参数个数,既可以作用于模板参数包T,也可以作用于函数参数包args。这个例子只是简单的将可变模版参数的个数打印出来,如果需要将参数包中的每个参数打印出来的话就需要通过其它方法了。展开函数参数包的方法一般有两种:一种是通过递归函数来展开参数包,另外一种是通过逗号表达式来展开参数包。

2.1.1递归函数方式展开参数包

通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数,递归终止函数正是用来终止递归的,来看看下面的例子。

#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;
}

上例会输出每一个参数,直到为空时输出empty。展开参数包的函数有两个,一个是递归函数,另外一个是递归终止函数,参数包Args…在展开的过程中递归调用自己,每调用一次参数包中的参数就会少一个,直到所有的参数都展开为止,当没有参数时,则调用非模板函数print终止递归过程。递归调用的过程是这样的:

print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
print();

上面的递归终止函数还可以用函数模板的偏特化版本:

//偏特化函数模板
template <class T> void print(T t)
{cout << "end " <<t<< endl;
}

修改递归终止函数后,上例中的调用过程是这样的:

print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);

程序输出结果:

parameter 1
parameter 2
parameter 3
end 4

2.1.2逗号表达式展开参数包

递归函数展开参数包是一种标准做法,也比较好理解,但也有一个缺点,就是必须要一个重载的递归终止函数,即必须要有一个同名的终止函数来终止递归,这样可能会感觉稍有不便。有没有一种更简单的方式呢?其实还有一种方法可以不通过递归方式来展开参数包,这种方式需要借助逗号表达式和初始化列表。比如前面print的例子可以改成这样:

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);

上面程序将打印出1,2,3,4。这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式,返回最后一个表达式结果,比如:

d = (a = b,c);

这个表达式会按顺序执行:b会先赋值给a,接着括号中的逗号表达式返回c的值,因此d将等于c。

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)
{initializer_list<int>{(f(std::forward<Args>(args)),0)...};
}
int main()
{expand([](int i){cout<<i<<endl;}, 1,2,3);
}

上面的例子将打印出每个参数,这里如果再使用C++14的新特性泛型lambda表达式的话,可以写更泛化的lambda表达式了:

expand([](auto i){cout<<i<<endl;}, 1,2.0,”test”);

2.2变参类模版

变参类模版是一个带可变模板参数的模板类,比如C++11中的元祖std::tuple就是一个可变模板类,它的定义如下:

template< class... Types> class tuple;

这个可变参数模板类可以携带任意类型任意个数的模板参数:

std::tuple<> tp;
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.5);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.5,"");

变参类模板的参数包展开方式和变参函数模板的展开方式不同,变参类模板的参数包展开需要通过模板特化和继承方式去展开,展开方式比变参函数模板要复杂。下面看一下展开变参类模板中的参数包的方法。

2.2.1偏特化与递归方式展开

变参类模板的展开一般需要定义两到三个类,包括类声明和偏特化的类模板。如下方式定义了一个基本的可变参数类模板:

//前向声明
template<typename... Args>
struct Sum;//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...>
{enum { value = Sum<First>::value + Sum<Rest...>::value };
};//递归终止
template<typename Last>
struct Sum<Last>
{enum { value = sizeof (Last) };
};int main()
{Sum<int, char> s;cout<<s.value<<endl;
}

程序输出5,即sizeof(int)+sizeof(char)。可以看到一个基本的可变参数模板应用类由三部分组成,前向声明、基本定义和递归终止类。实际上三段式的定义也可以改为两段式,可以将前向声明去掉,这样定义:

template<typename First, typename... Rest>
struct Sum
{enum { value = Sum<First>::value + Sum<Rest...>::value };
};template<typename Last>
struct Sum<Last>
{enum{ value = sizeof(Last) };
};

递归终止模板类可以有多种写法,比如上例的递归终止模板类还可以这样写:

template<typename... Args> struct sum;
template<typename First, typenameLast>
struct sum<First, Last>
{ enum{ value = sizeof(First) +sizeof(Last) };
};

在展开到最后两个参数时终止。还可以在展开到0个参数时终止:

template<>struct sum<> { enum{ value = 0 }; };

2.2.2继承方式展开

还可以通过继承方式来展开参数包,比如下面的例子就是通过继承的方式去展开参数包:

//整型序列的定义
template<int...> struct IndexSeq {};//继承方式,开始展开参数包
template<int N, int... Indexes> struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...> {};// 模板特化,终止展开参数包的条件
template<int... Indexes> struct MakeIndexes<0, Indexes...>
{typedef IndexSeq<Indexes...> type;
};int main()
{using T = MakeIndexes<3>::type;cout << typeid(T).name() << endl;return 0;
}

其中MakeIndexes的作用是为了生成一个可变参数模板类的整数序列,最终输出的类型是:struct IndexSeq<0,1,2>。

MakeIndexes继承于自身的一个特化的模板类,这个特化的模板类同时也在展开参数包,这个展开过程是通过继承发起的,直到遇到特化的终止条件展开过程才结束。MakeIndexes<1,2,3>::type的展开过程是这样的:

MakeIndexes<3> : MakeIndexes<2, 2>{}
MakeIndexes<2, 2> : MakeIndexes<1, 1, 2>{}
MakeIndexes<1, 1, 2> : MakeIndexes<0, 0, 1, 2>
{typedef IndexSeq<0, 1, 2> type;
}

通过不断的继承递归调用,最终得到整型序列IndexSeq<0, 1, 2>。

如果不希望通过继承方式去生成整形序列,则可以通过下面的方式生成。

template<int N, int... Indexes>
struct MakeIndexes3
{using type = typename MakeIndexes3<N - 1, N - 1, Indexes...>::type;
};template<int... Indexes>
struct MakeIndexes3<0, Indexes...>
{typedef IndexSeq<Indexes...> type;
};

3.变参模板的应用

我们可以利用递归以及偏特化等方法来展开模板参数包,那么实际当中我们会怎么去使用它呢?我们可以用变参模板来消除一些重复的代码以及实现一些高级功能,下面我们来看看可变参模板的一些应用。

3.1消除重复代码

C++11之前如果要写一个泛化的工厂函数,这个工厂函数能接受任意类型的入参,并且参数个数要能满足大部分的应用需求的话,我们不得不定义很多重复的模版定义,比如下面的代码:

template<typename T> T* Instance()
{return new T();
}template<typename T, typename T0> T* Instance(T0 arg0)
{return new T(arg0);
}template<typename T, typename T0, typename T1> T* Instance(T0 arg0, T1 arg1)
{return new T(arg0, arg1);
}template<typename T, typename T0, typename T1, typename T2>
T* Instance(T0 arg0, T1 arg1, T2 arg2)
{return new T(arg0, arg1, arg2);
}struct A
{A(int){}
};struct B
{B(int,double){}
};
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);

可以看到这个泛型工厂函数存在大量的重复的模板定义,并且限定了模板参数。用可变模板参数可以消除重复,同时去掉参数个数的限制,代码很简洁, 通过可变参数模版优化后的工厂函数如下:

template<typename T,typename...  Args> T* Instance(Args&&... args)
{return new T(std::forward<Args>(args)...);
};
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);

3.2实现泛化的delegate

C++中没有类似C#的委托,我们可以借助可变模版参数来实现一个。C#中的委托的基本用法是这样的:

delegate int AggregateDelegate(int x, int y);//声明委托类型int Add(int x, int y){return x+y;}
int Sub(int x, int y){return x-y;}AggregateDelegate add = Add;
add(1,2);//调用委托对象求和
AggregateDelegate sub = Sub;
sub(2,1);// 调用委托对象相减

C#中的委托的使用需要先定义一个委托类型,这个委托类型不能泛化,即委托类型一旦声明之后就不能再用来接受其它类型的函数了,比如这样用:

int Fun(int x, int y, int z){return x+y+z;}
int Fun1(string s, string r){return s.Length+r.Length; }
AggregateDelegate fun = Fun; //编译报错,只能赋值相同类型的函数
AggregateDelegate fun1 = Fun1;//编译报错,参数类型不匹配

这里不能泛化的原因是声明委托类型的时候就限定了参数类型和个数,在C++11里不存在这个问题了,因为有了可变模版参数,它就代表了任意类型和个数的参数了,下面让我们来看一下如何实现一个功能更加泛化的C++版本的委托(这里为了简单起见只处理成员函数的情况,并且忽略const、volatile成员函数的处理)。

template <class T, class R, typename... Args>
class  MyDelegate
{
public:MyDelegate(T* t, R(T::*f)(Args...)) :m_t(t), m_f(f) {}R operator()(Args&&... args){return (m_t->*m_f)(std::forward<Args>(args) ...);}private:T * m_t;R(T::*m_f)(Args...);
};template <class T, class R, typename... Args>
MyDelegate<T, R, Args...> CreateDelegate(T* t, R (T::*f)(Args...))
{return MyDelegate<T, R, Args...>(t, f);
}struct A
{void Fun(int i) { cout << i << endl; }void Fun1(int i, double j) { cout << i + j << endl; }
};int main()
{A a;auto d = CreateDelegate(&a, &A::Fun);     //创建委托d(1);                                     //调用委托,将输出1auto d1 = CreateDelegate(&a, &A::Fun1);  //创建委托d1(1, 2.5);                                   //调用委托,将输出3.5
}

MyDelegate实现的关键是内部定义了一个能接受任意类型和个数参数的“万能函数”:R (T::*m_f)(Args…),正是由于可变模版参数的特性,所以我们才能够让这个m_f接受任意参数。

4.总结

使用变参模板能够简化代码,正确使用的关键是如何展开参数包,展开参数包的过程是很精妙的,体现了泛化之美、递归之美,正是因为它具有神奇的“魔力”,所以我们可以更泛化地去处理问题,比如用它来消除重复的模版定义,用它来定义一个能接受任意参数的“万能函数”等。其实,可变模版参数的作用远不止文中列举的那些作用,它还可以和其它C++11特性结合起来,比如type_traits、std::tuple等特性,发挥更加强大的威力。


参考文献

[1]泛化之美–C++11可变模版参数的妙用

C++11 变参模板相关推荐

  1. 变参模板、完美转发和emplace

    文章目录 1 变参模板.完美转发和emplace 1 变参模板.完美转发和emplace 变参模板:使得 emplace 可以接受任意参数,这样就可以适用于任意对象的构建. 完美转发 :使得接收下来的 ...

  2. 可变参C API va_list,va_start,va_arg_va_end以及c++可变参模板

    文章目录 C变参API C变参API函数原型 C变参API实现源码 C变参API应用实例 C 变参函数缺点 C++变参实现 方法 initializer_list 形参 可变参数模板 C变参API C ...

  3. Th4.7:可变参模板

    本小节回顾的知识点分别是可变参模板. 今天总结的知识分为以下3个大点: (1)可变惨模板概念 (2)可变参函数模板     (2.1)简单范例     (2.2)参数包的展开 (3)可变参类模板    ...

  4. C++11的模板改进

    C++11关于模板有一些细节的改进: 模板的右尖括号 模板的别名 函数模板的默认模板参数 模板的右尖括号 C++11之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割, ...

  5. c++11 之模板定义别名(using)

    C++11标准中可以为模板定义别名,比如 template<typename T> using ptr=std::shared_ptr<T>; //这里模板定义ptr<T ...

  6. [C++11]对模板右尖括号的优化

    在泛型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(>>)会被编译器解析成右移操作符,而不是模板参数表的结束. C++11改进了编译器的解析规则,尽可能地将多个右尖号( ...

  7. [C++11]函数模板的默认模板参数

    在C++11中添加了对函数模板默认参数的支持. 代码如下: #include<iostream> using namespace std;template<typename T = ...

  8. C++11 函数模板的默认模板参数

    1.函数模板默认模板参数简介 函数模板与类模板在 C++98 一起被引入,因种种原因,类模板可以拥有默认模板参数,而函数模板不可以.从 C++11 开始,这个限制被解除了,即函数模板同样可以拥有默认模 ...

  9. C++11标准模板(STL)- 算法(std::set_symmetric_difference)

    定义于头文件 <algorithm> 算法库提供大量用途的函数(例如查找.排序.计数.操作),它们在元素范围上操作.注意范围定义为 [first, last) ,其中 last 指代要查询 ...

最新文章

  1. error RC1015: cannot open include file 'afxres.h'. 的解决办法
  2. python添加数组元素_Python列表附录–如何向数组添加元素,并附带示例说明
  3. bindService初步了解
  4. 【腾讯Bugly干货分享】动态链接库加载原理及HotFix方案介绍
  5. 文件内容批量修改工具
  6. 深入理解Kubernetes容器网络
  7. python如何处理spark上的数据_Pyspark获取并处理RDD数据代码实例
  8. 记录一下flex布局左边固定,右边100%
  9. 整理: JAVA错误处理集锦
  10. 用户计算机安装有512m内存,安装OFFICESCAN客户端(计算机内存要求512M以上)
  11. 《大型网站技术架构:核心原理与案例分析》.pdf——架构系列必看20本技术书籍
  12. 数据结构与算法笔记 二叉树、二叉搜索树、二叉平衡树的区分与关系
  13. c语言函数求圆面积,C语言编写函数,计算圆面积.
  14. 高通WLAN稳定和功耗分析--目前高通项目支持的功耗策略
  15. fxml设置背景_JavaFX Scene Builder使用总结
  16. IDEA Auto build completed with errors解决办法
  17. verilog学习笔记:简单的数据选择器modelsim仿真
  18. 码距与检错或纠错能力的关系
  19. 如何开发HTML编辑器
  20. Cris 玩转大数据系列之 Hadoop HA 实现

热门文章

  1. 出于安全考虑,谷歌禁用三款 Linux web 浏览器登录其服务
  2. 年薪30W前端程序员,需要吃透的前端书籍推荐
  3. 关于telnet的安装
  4. CSS Sprite精灵图如何缩放大小
  5. 【讨论帖】你认为怎么注释是比较合理妥当的方式
  6. SpringCloud的EurekaClient : 客户端应用访问注册的微服务(无断路器场景)
  7. 设计模式系列 12-- 职责链模式
  8. Qt Creator快捷键大全
  9. linux 一句话备忘
  10. 预防SQL注入攻击之我见 转