C++程序员应了解的那些事(88)之 变参模板

 

目录

一、变参模板概述

二、可变模版参数的展开

2.1 变参函数模版

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

2.1.2 逗号表达式展开参数包

2.2 变参类模版

2.2.1 偏特化与递归方式展开

2.2.2 继承方式展开

三、变参模板的应用

3.1 消除重复代码

3.2 实现泛化的delegate

四、总结

附一、变参模板练习

print函数:

max函数:

注1: C++11之后已经支持了多个数求最大值的操作,使用initializer_list。  std::cout << max({ 57,48,60,100,20,18 });

sum函数:

注2 :变参模板函数递归,特化要放在前面,因为有时候会出现找不到特例化版本的问题!【但也可以通过前置声明解决】

附二、可变参数模板函数show调用示例

1、可变参数模板函数声明和定义

2、可变参数模板函数调用示例

附三、模板函数的编译过程


一、变参模板概述

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

二、可变模版参数的展开

        可变模板参数和普通模板参数的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在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终止递归过程。

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

//偏特化函数模板
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...> //如果没有上面的前置声明,会报错:'Sum' is not a class template
                           //主模板的声明中不允许使用模板参数列表C/C++(840)
{
    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)。※ 可以看到一个基本的可变参数模板应用类由三部分组成,前向声明、基本定义和递归终止类。

       第一部分:前向声明,声明这个sum类是一个可变参数模板类。这个前向声明要求sum的模板参数至少有一个,因为可变参数模板中的模板参数可以有0个,有时候0个模板参数没有意义,就可以通过上面的声明方式来限定模板参数不能为0个。

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 (First) };
}

       实际上三段式的定义也可以改为两段式,可以将前向声明去掉,这样定义:

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, typename Last>
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.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);								                      //调用委托,将输出1
    auto d1 = CreateDelegate(&a, &A::Fun1); 	//创建委托
    d1(1, 2.5);                               //调用委托,将输出3.5
}

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

四、总结

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

附一、变参模板练习

Variadic Template是C++11的一个很重要的特性,变体现在两个方面:

(1)参数个数:利用参数个数逐一递减的特性,实现递归调用;

(2)参数类型:参数个数逐一递减导致参数类型也逐一递减;

两个注意点

(1)递归调用

(2)递归终止:使用重载的办法终止递归调用;

print函数:

///  Variadic template

//重载的递归终止函数
void printX() {
}
 
template<typename T,typename...Types>
void printX(const T& firstArg, const Types&...args) {
	cout << firstArg << endl;
	printX(args...);
}
 
int main()
{
	printX(7.5, "hello", bitset<16>(377), 42);
	return 0;
}

max函数:

//重载的递归终止条件
int maximun(int n){
	return n;
} 
template<typename...Args>
int maximun(int n,Args...args){
	return std::max(n,maximun(args...));
}
int main()
{
	//C++11之后已经支持了多个数求最大值的操作,使用initializer_list
	//cout << max({ 57,48,60,100,20,18 });
	cout<<maximun(57,48,60,100,20,18)<<endl;
	return 0;
}

注1: C++11之后已经支持了多个数求最大值的操作,使用initializer_list。  std::cout << max({ 57,48,60,100,20,18 });

sum函数:

template<typename Arg>
int sum(Arg f)
{
    return f;
}
template<typename Arg, typename... Ts>
int sum(Arg f, Arg s, Ts... All)
{
    return f + sum(s,all...);
}

注2 :变参模板函数递归,特化要放在前面,因为有时候会出现找不到特例化版本的问题!【但也可以通过前置声明解决】

举例如下:

template<typename Arg, typename... Ts>
int sum(Arg f, Arg s, Ts... all)
{
    return f + sum(s,all...);
}
//特化版 终止函数
template<typename Arg>
int sum(Arg f)
{
    return f;
}
int main()
{
    std::cout << sum(1,2,3);
    return 0;
}

报错如下:

main.cpp: In instantiation of 'int sum(Arg, Arg, Ts ...) [with Arg = int; Ts = {}]':
main.cpp:4:19:   required from 'int sum(Arg, Arg, Ts ...) [with Arg = int; Ts = {int}]'
main.cpp:14:27:   required from here
main.cpp:4:19: error: no matching function for call to 'sum(int&)'
     return f + sum(s,all...);
                ~~~^~~~~~~~~~
main.cpp:2:5: note: candidate: 'template<class Arg, class ... Ts> int sum(Arg, Arg, Ts ...)'
 int sum(Arg f, Arg s, Ts... all)
     ^~~
main.cpp:2:5: note:   template argument deduction/substitution failed:
main.cpp:4:19: note:   candidate expects at least 2 arguments, 1 provided
     return f + sum(s,all...);
                ~~~^~~~~~~~~~

前置声明解决:

//前置声明
template<typename Arg>
int sum(Arg f);

template<typename Arg, typename... Ts>
int sum(Arg f, Arg s, Ts... all)
{
    return f + sum(s,all...);
}
//特化版 终止函数
template<typename Arg>
int sum(Arg f)
{
    return f;
}
int main()
{
    std::cout << sum(1,2,3);
    return 0;
}

附二、可变参数模板函数show调用示例

1、可变参数模板函数声明和定义

template<typename... Args>  //Args是一个模板参数包
void Show(Args... args)     //args是一个函数参数包
{
    //函数功能
    return;
}

       由上面代码可知,Args是一个模板参数包,args是一个函数参数包,都表示一系列的参数。可变参数模板函数一般用于可变参数输出,可变参数args的调用不能用args[2]方式调用,可以采用递归展开和非递归展开。下面示例给出了两种递归展开和非递归展开用法。

2、可变参数模板函数调用示例

/*定义和声明一个多参数模板函数,递归方式展开*/
void Show1(){}   //定义零个参数函数,递归调用到零个参数函数时。调用该函数,然后终止继续递归
template<typename T,typename ... Args>  //Args是一个模板参数包
void Show1(const T &value,const Args &... args)     //args是一个函数参数包
{
    qDebug()<<"Show1 value=="<<value;
    Show1(args...);  //递归调用,依次展开每个参数。不能用args[2]方式调用,只能依次展开
}
 
/*定义和声明一个多参数模板函数,非递归方式展开*/
template<typename T>
void Print(T arg)
{
    qDebug()<<"Show2 Print(arg)=="<<arg;
}
template<typename ... Args>
void Show2(Args ... args)
{
    int a[]={(Print(args),0)...};
    for(uint i=0;i<sizeof a;i++)
        qDebug()<<QObject::tr("Show2 a[%1]==").arg(i)<<a[i];
}
 
/*调用按钮函数*/
void Widget::on_pushButton_2_clicked()
{
    Show1(1,2.0,"33","string");
    Show2(66,8888.0,"dsd","sd545sd2s");
}

附三、模板函数的编译过程

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值