1. 程式人生 > 程式設計 >詳解C++11 變參模板

詳解C++11 變參模板

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,4);
print(2,4);
print(3,4);
print(4);
print();

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

上面程式將打印出1,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),(printarg(arg3),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,3);
}

上面的例子將打印出每個引數,這裡如果再使用C++14的新特性泛型lambda表示式的話,可以寫更泛化的lambda表示式了:

expand([](auto i){cout<<i<<endl;},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,2>。

MakeIndexes繼承於自身的一個特化的模板類,這個特化的模板類同時也在展開引數包,這個展開過程是通過繼承發起的,直到遇到特化的終止條件展開過程才結束。MakeIndexes<1,3>::type的展開過程是這樣的:

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

通過不斷的繼承遞迴呼叫,最終得到整型序列IndexSeq<0,2>。

如果不希望通過繼承方式去生成整形序列,則可以通過下面的方式生成。

template<int N,int... Indexes>
struct MakeIndexes3
{
  using type = typename MakeIndexes3<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 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,typename... Args>
MyDelegate<T,R,Args...> CreateDelegate(T* t,R (T::*f)(Args...))
{
  return MyDelegate<T,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接受任意引數。

4.總結

使用變參模板能夠簡化程式碼,正確使用的關鍵是如何展開引數包,展開引數包的過程是很精妙的,體現了泛化之美、遞迴之美,正是因為它具有神奇的“魔力”,所以我們可以更泛化的去處理問題,比如用它來消除重複的模版定義,用它來定義一個能接受任意引數的“萬能函式”等。其實,可變模版引數的作用遠不止文中列舉的那些作用,它還可以和其它C++11特性結合起來,比如type_traits、std::tuple等特性,發揮更加強大的威力。

以上就是詳解C++11 變參模板的詳細內容,更多關於C++11 變參模板的資料請關注我們其它相關文章!