標籤:特化 operator length 無法 進一步 struct post 一個 特點
泛化之美--C++11可變模版參數的妙用1概述
C++11的新特性--可變模版參數(variadic templates)是C++11新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數。相比C++98/03,類模版和函數模版中只能含固定數量的模版參數,可變模版參數無疑是一個巨大的改進。然而由於可變模版參數比較抽象,使用起來需要一定的技巧,所以它也是C++11中最難理解和掌握的特性之一。雖然掌握可變模版參數有一定難度,但是它卻是C++11中最有意思的一個特性,本文希望帶領讀者由淺入深的認識和掌握這一特性,同時也會通過一些執行個體來展示可變參數模版的一些用法。
2可變模版參數的展開
可變參數模板和普通模板的語義是一樣的,只是寫法上稍有區別,聲明可變參數模板時需要在typename或class後面帶上省略符號“...”。比如我們常常這樣聲明一個可變模版參數: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(); //0f(1, 2); //2f(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++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之前如果要寫一個泛化的工廠函數,這個工廠函數能接受任意類型的入參,並且參數個數要能滿足大部分的應用需求的話,我們不得不定義很多重複的模版定義,比如下面的代碼:
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);
可以看到這個泛型工廠函數存在大量的重複的模板定義,並且限定了模板參數。用可變模板參數可以消除重複,同時去掉參數個數的限制,代碼很簡潔, 通過可變參數模版最佳化後的工廠函數如下:
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實現的關鍵是內部定義了一個能接受任意類型和個數參數的“萬能函數”:R (T::*m_f)(Args...),正是由於可變模版參數的特性,所以我們才能夠讓這個m_f接受任意參數。
5總結
使用可變模版參數的這些技巧相信讀者看了會有耳目一新之感,使用可變模版參數的關鍵是如何展開參數包,展開參數包的過程是很精妙的,體現了泛化之美、遞迴之美,正是因為它具有神奇的“魔力”,所以我們可以更泛化的去處理問題,比如用它來消除重複的模版定義,用它來定義一個能接受任意參數的“萬能函數”等。其實,可變模版參數的作用遠不止文中列舉的那些作用,它還可以和其它C++11特性結合起來,比如type_traits、std::tuple等特性,發揮更加強大的威力,將在後面模板元編程的應用中介紹。
-C++11可變模版參數(轉載)