所以它也是C++11中最难理解和掌握的特性之一

作者:澳门娱乐

1概述

C++11的新性格--可变模版参数(variadic templates)是C++11骤增的最有力的特点之一,它对参数举行了冲天泛化,它能表示0到自由个数、任意等级次序的参数。相比较C++98/03,类模版和函数模版中只可以含一定数量的模板参数,可变模版参数无疑是一个有才能的人的革新。然则由于可变模版参数比较抽象,使用起来须求自然的才能,所以它也是C++1第11中学最难通晓和垄断(monopoly)的性子之一。尽管驾驭可变模版参数有明确难度,可是它却是C++1第11中学最棒玩的一个特色,本文希望指引读者奉公守法的认知和左右这一风味,同时也会因而一些实例来彰显可变参数模版的部分用法。

2可变模版参数的开展

可变参数模板和平常模板的语义是一样的,只是写法上稍有分别,评释可变参数模板时供给在typename或class前边带上省略号“...”。譬如大家平日那样声贝拉米(Nutrilon)个可变模版参数:template<typename...>或许template<class...>,三个超人的可变模版参数的概念是这么的:

template <class... T>
void f(T... args);

  上面的可变模版参数的定义其中,省略号的功用有八个:
1.扬言贰个参数包T... args,这一个参数包中能够包涵0到自由个模板参数;
2.在模板定义的入手,能够将参数包进行成二个一个独立的参数。

  上面包车型客车参数args前边有省略号,所以它便是三个可变模版参数,大家把带省略号的参数称为“参数包”,它个中含有了0到N(N>=0)个模版参数。大家鞭长莫及直接获得参数包args中的每一种参数的,只可以通过开展参数包的主意来获得参数包中的每一个参数,那是利用可变模版参数的三个主要特色,也是最大的难题,即如何进行可变模版参数。

  可变模版参数和平凡的模版参数语义是一致的,所以能够动用于函数和类,就能够变模版参数函数和可变模版参数类,但是,模版函数不协助偏特化,所以可变模版参数函数和可变模版参数类举办可变模版参数的措施还不尽一样,上边大家来分别拜会他们实行可变模版参数的不二等秘书诀。

2.1可变模版参数函数

一个简单的可变模版参数函数:

template <class... T>
void f(T... args)
{    
    cout << sizeof...(args) << endl; //打印变参的个数
}

f();        //0
f(1, 2);    //2
f(1, 2.5, "");    //3

地点的例子中,f()未有传来参数,所以参数包为空,输出的size为0,前边一遍调用各自传入多少个和八个参数,故输出的size分别为2和3。由于可变模版参数的项目和个数是不定点的,所以大家能够传任性类型和个数的参数给函数f。这些事例只是简短的将可变模版参数的个数打字与印刷出来,假如大家需求将参数包中的每一个参数打字与印刷出来的话就须求经过有个别艺术了。打开可变模版参数函数的方法一般有两种:一种是由此递归函数来举行参数包,其它一种是通过逗号说明式来进展参数包。上边来拜访如何用那二种艺术来开展参数包。

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 << t << endl;
}

修改递归终止函数后,上例中的调用进度是那般的:

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

当参数包举办到最后二个参数时递归截至。再看多个由此可变模版参数求和的事例:

template<typename T>
T sum(T t)
{
    return t;
}
template<typename T, typename ... Types>
T sum (T first, Types ... rest)
{
    return first + sum<T>(rest...);
}

sum(1,2,3,4); //10

sum在开展参数包的进程上校各种参数相加求和,参数的扩充格局和日前的打字与印刷参数包的法子是毫发不爽的。

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) 
{
  //这里用到了完美转发,关于完美转发,读者可以参考笔者在上一期程序员中的文章《通过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”);

2.2可变模版参数类

可变参数模板类是三个带可变模板参数的模板类,比方C++1第11中学的元祖std::tuple正是贰个可变模板类,它的定义如下:

template< class... Types >
class tuple;

其一可变参数模板类能够教导大肆档次放肆个数的模版参数:

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, “”);

可变参数模板的模版参数个数可以为0个,所以上面包车型大巴概念也是也是法定的:

std::tuple<> tp;

可变参数模板类的参数包进行的办法和可变参数模板函数的举行情势各异,可变参数模板类的参数包举办须求通过沙盘特化和承袭格局去开展,张开药形式比可变参数模板函数要复杂。下边大家来看一下拓宽可变模版参数类中的参数包的艺术。

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

  那一个Sum类的效果是在编写翻译期计算出参数包中参数类型的size之和,通过sum<int,double,short>::value就足以博得那3个类型的size之和为14。那是一个简练的经过可变参数模板类总结的例子,能够看出多个主干的可变参数模板应用类由三有个别组成,第一某些是:

template<typename... Args> struct sum

 

它是前向注明,注解这几个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... Args>struct sum;

 

其一前向注脚须要sum的沙盘参数至少有三个,因为可变参数模板中的模板参数能够有0个,有的时候候0个模板参数未有意义,就可以经过地点的扬言格局来界定模板参数不可能为0个。上面包车型地铁这种三段式的定义也足以改为两段式的,能够将前向表明去掉,那样定义:

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

 

仍是能够利用std::integral_constant来清除枚举定义value。利用std::integral_constant能够拿走编译期常量的特色,能够将前方的sum例子改为这么:

//前向声明
template<typename First, typename... Args>
struct Sum;

//基本定义
template<typename First, typename... Rest>
struct Sum<First, Rest...> : std::integral_constant<int, Sum<First>::value + Sum<Rest...>::value>
{
};

//递归终止
template<typename Last>
struct Sum<Last> : std::integral_constant<int, sizeof(Last)>
{
};
sum<int,double,short>::value;//值为14

 

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...>
{
    typedefIndexSeq<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可变参数模版解决重复代码

C++11事先借使要写一个泛化的工厂函数,那一个工厂函数能承受跋扈档案的次序的入参,而且参数个数要能知足大多数的施用必要的话,大家只能定义比非常多重复的沙盘定义,举例下边包车型客车代码:

图片 1图片 2

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

template<typename T, typename T0, typename T1, typename T2, typename T3>
T* Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3)
{
    return new T(arg0, arg1, arg2, arg3);
}

template<typename T, typename T0, typename T1, typename T2, typename T3, typename T4>
T* Instance(T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
    return new T(arg0, arg1, arg2, arg3, arg4);
}
struct A
{
    A(int){}
};

struct B
{
    B(int,double){}
};
A* pa = Instance<A>(1);
B* pb = Instance<B>(1,2);

View Code

能够见见那个泛型工厂函数存在大量的再次的沙盘定义,并且限制了模版参数。用可变模板参数能够祛除重复,同有时候去掉参数个数的限定,代码很轻巧, 通过可变参数模版优化后的工厂函数如下:

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

4可变参数模版完成泛化的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和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完成的关键是中间定义了一个能接受大肆档案的次序和个数参数的“万能函数”:奥迪Q5  (T::*m_f)(Args...),便是由于可变模版参数的风味,所以我们技术够让那几个m_f接受狂妄参数。

5总结

运用可变模版参数的这么些技术相信读者看了会有面目全非之感,使用可变模版参数的关键是什么样开展参数包,展开参数包的长河是很精密的,显示了泛化之美、递归之美,正是因为它具备奇妙的“吸引力”,所以我们得以更泛化的去管理难点,比如用它来驱除重复的沙盘定义,用它来定义贰个能经受大肆参数的“万能函数”等。其实,可变模版参数的意义远不只有文中列举的那八个功用,它还能和另外C++11风味结合起来,比方type_traits、std::tuple等性格,发挥更为有力的威力,就要背后模板元编制程序的行使中介绍。

 

正文曾发布于《程序猿》二零一五年1月刊。转发请申明出处。

后记:正文的内容首要来自于本身在店堂里面培养磨练的一次课程,因为十分的多人对C++11可变模板参数搞不清也许理解得不深入,所以本身感觉有不可或缺拿出去分享一下,让越多的人收看,就照管了一晃发到工程师杂志了,作者相信读者看完以往对可变模板参数会有宏观深切的垂询。

 

本文由澳门娱乐6165发布,转载请注明来源

关键词: