詳解C++11 變參模板
1.概述
變參模板(variadic template)是C++11新增的最強大的特性之一,它對參數(shù)進行了高度泛化,它能表示0到任意個數(shù)、任意類型的參數(shù)。相比C++98/03,類模版和函數(shù)模版中只能含固定數(shù)量的模版參數(shù),可變模版參數(shù)無疑是一個巨大的改進。然而由于可變模版參數(shù)比較抽象,使用起來需要一定的技巧,掌握也存在一定的難度。
2.可變模版參數(shù)的展開
可變模板參數(shù)和普通模板參數(shù)的語義是一樣的,只是寫法上稍有區(qū)別,聲明可變參數(shù)模板時需要在typename或class后面帶上省略號“…”??勺儏?shù)模版的定義形式如下:
//可變參數(shù)函數(shù)模板 template<typename... T> void f(T... args); //可變參數(shù)類模板 template<typename... T> class ClassFoo;
上面的參數(shù)中,T為模板參數(shù)包(template parameter pack),args為函數(shù)參數(shù)包(function parameter pack),參數(shù)包里面包含了0到N(N>=0)個參數(shù)。我們無法直接獲取參數(shù)包中的每個參數(shù)的,只能通過展開參數(shù)包的方式,這是使用可變參數(shù)模版的一個主要特點,也是最大的難點。
可變模版參數(shù)和普通的模版參數(shù)語義是一致的,可以應用于函數(shù)和類,然而,函數(shù)模版不支持偏特化,所以可變參數(shù)函數(shù)模版和可變參數(shù)類模版展開參數(shù)包的方法有所不同,下面我們來分別看看他們參數(shù)包展開的方法。
2.1變參函數(shù)模版
一個簡單的變參函數(shù)模板。
template <class... T> void f(T... args) { cout << sizeof...(T) <<" "<< sizeof...(args) << endl; //打印函數(shù)參數(shù)包中參數(shù)個數(shù) } f(); //0 0 f(1, 1.2); //2 2 f(1, 2.3, ""); //3 3
sizeof…運算符的作用是計算參數(shù)包中的參數(shù)個數(shù),既可以作用于模板參數(shù)包T,也可以作用于函數(shù)參數(shù)包args。這個例子只是簡單的將可變模版參數(shù)的個數(shù)打印出來,如果需要將參數(shù)包中的每個參數(shù)打印出來的話就需要通過其它方法了。展開函數(shù)參數(shù)包的方法一般有兩種:一種是通過遞歸函數(shù)來展開參數(shù)包,另外一種是通過逗號表達式來展開參數(shù)包。
2.1.1遞歸函數(shù)方式展開參數(shù)包
通過遞歸函數(shù)展開參數(shù)包,需要提供一個參數(shù)包展開的函數(shù)和一個遞歸終止函數(shù),遞歸終止函數(shù)正是用來終止遞歸的,來看看下面的例子。
#include <iostream> using namespace std; //遞歸終止函數(shù) void print() { cout << "empty" << endl; } //展開函數(shù) 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; }
上例會輸出每一個參數(shù),直到為空時輸出empty。展開參數(shù)包的函數(shù)有兩個,一個是遞歸函數(shù),另外一個是遞歸終止函數(shù),參數(shù)包Args…在展開的過程中遞歸調用自己,每調用一次參數(shù)包中的參數(shù)就會少一個,直到所有的參數(shù)都展開為止,當沒有參數(shù)時,則調用非模板函數(shù)print終止遞歸過程。遞歸調用的過程是這樣的:
print(1,2,3,4);
print(2,3,4);
print(3,4);
print(4);
print();
2.1.2逗號表達式展開參數(shù)包
遞歸函數(shù)展開參數(shù)包是一種標準做法,也比較好理解,但也有一個缺點,就是必須要一個重載的遞歸終止函數(shù),即必須要有一個同名的終止函數(shù)來終止遞歸,這樣可能會感覺稍有不便。有沒有一種更簡單的方式呢?其實還有一種方法可以不通過遞歸方式來展開參數(shù)包,這種方式需要借助逗號表達式和初始化列表。比如前面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。這種展開參數(shù)包的方式,不需要通過遞歸終止函數(shù),是直接在expand函數(shù)體中展開的, printarg不是一個遞歸終止函數(shù),只是一個處理參數(shù)包中每一個參數(shù)的函數(shù)。這種就地展開參數(shù)包的方式實現(xiàn)的關鍵是逗號表達式。我們知道逗號表達式會按順序執(zhí)行逗號前面的表達式,返回最后一個表達式結果,比如:
d = (a = b,c);
這個表達式會按順序執(zhí)行:b會先賦值給a,接著括號中的逗號表達式返回c的值,因此d將等于c。
expand函數(shù)中的逗號表達式:(printarg(args), 0),也是按照這個執(zhí)行順序,先執(zhí)行printarg(args),再得到逗號表達式的結果0。同時還用到了C++11的另外一個特性——列表初始化,通過列表初始化來初始化一個變長數(shù)組, {(printarg(args), 0)…}將會展開成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc… ),最終會創(chuàng)建一個元素值都為0的數(shù)組int arr[sizeof…(Args)]。由于是逗號表達式,在創(chuàng)建數(shù)組的過程中會先執(zhí)行逗號表達式前面的部分printarg(args)打印出參數(shù),也就是說在構造int數(shù)組的過程中就將參數(shù)包展開了,這個數(shù)組的目的純粹是為了在數(shù)組構造的過程展開參數(shù)包。我們可以把上面的例子再進一步改進一下,將函數(shù)作為參數(shù),就可以支持lambda表達式了,從而可以少寫一個遞歸終止函數(shù)了,具體代碼如下:
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); }
上面的例子將打印出每個參數(shù),這里如果再使用C++14的新特性泛型lambda表達式的話,可以寫更泛化的lambda表達式了:
expand([](auto i){cout<<i<<endl;}, 1,2.0,”test”);
2.2變參類模版
變參類模版是一個帶可變模板參數(shù)的模板類,比如C++11中的元祖std::tuple就是一個可變模板類,它的定義如下:
template< class... Types> class tuple;
這個可變參數(shù)模板類可以攜帶任意類型任意個數(shù)的模板參數(shù):
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,"");
變參類模板的參數(shù)包展開方式和變參函數(shù)模板的展開方式不同,變參類模板的參數(shù)包展開需要通過模板特化和繼承方式去展開,展開方式比變參函數(shù)模板要復雜。下面看一下展開變參類模板中的參數(shù)包的方法。
2.2.1偏特化與遞歸方式展開
變參類模板的展開一般需要定義兩到三個類,包括類聲明和偏特化的類模板。如下方式定義了一個基本的可變參數(shù)類模板:
//前向聲明 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)。可以看到一個基本的可變參數(shù)模板應用類由三部分組成,前向聲明、基本定義和遞歸終止類。實際上三段式的定義也可以改為兩段式,可以將前向聲明去掉,這樣定義:
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) }; };
在展開到最后兩個參數(shù)時終止。還可以在展開到0個參數(shù)時終止:
template<>struct sum<> { enum{ value = 0 }; };
2.2.2繼承方式展開
還可以通過繼承方式來展開參數(shù)包,比如下面的例子就是通過繼承的方式去展開參數(shù)包:
//整型序列的定義 template<int...> struct IndexSeq {}; //繼承方式,開始展開參數(shù)包 template<int N, int... Indexes> struct MakeIndexes : MakeIndexes<N - 1, N - 1, Indexes...> {}; // 模板特化,終止展開參數(shù)包的條件 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的作用是為了生成一個可變參數(shù)模板類的整數(shù)序列,最終輸出的類型是:struct IndexSeq<0,1,2>。
MakeIndexes繼承于自身的一個特化的模板類,這個特化的模板類同時也在展開參數(shù)包,這個展開過程是通過繼承發(fā)起的,直到遇到特化的終止條件展開過程才結束。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.變參模板的應用
我們可以利用遞歸以及偏特化等方法來展開模板參數(shù)包,那么實際當中我們會怎么去使用它呢?我們可以用變參模板來消除一些重復的代碼以及實現(xiàn)一些高級功能,下面我們來看看可變參模板的一些應用。
3.1消除重復代碼
C++11之前如果要寫一個泛化的工廠函數(shù),這個工廠函數(shù)能接受任意類型的入參,并且參數(shù)個數(shù)要能滿足大部分的應用需求的話,我們不得不定義很多重復的模版定義,比如下面的代碼:
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);
可以看到這個泛型工廠函數(shù)存在大量的重復的模板定義,并且限定了模板參數(shù)。用可變模板參數(shù)可以消除重復,同時去掉參數(shù)個數(shù)的限制,代碼很簡潔, 通過可變參數(shù)模版優(yōu)化后的工廠函數(shù)如下:
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實現(xiàn)泛化的delegate
C++中沒有類似C#的委托,我們可以借助可變模版參數(shù)來實現(xiàn)一個。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#中的委托的使用需要先定義一個委托類型,這個委托類型不能泛化,即委托類型一旦聲明之后就不能再用來接受其它類型的函數(shù)了,比如這樣用:
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; //編譯報錯,只能賦值相同類型的函數(shù) AggregateDelegate fun1 = Fun1;//編譯報錯,參數(shù)類型不匹配
這里不能泛化的原因是聲明委托類型的時候就限定了參數(shù)類型和個數(shù),在C++11里不存在這個問題了,因為有了可變模版參數(shù),它就代表了任意類型和個數(shù)的參數(shù)了,下面讓我們來看一下如何實現(xiàn)一個功能更加泛化的C++版本的委托(這里為了簡單起見只處理成員函數(shù)的情況,并且忽略const、volatile成員函數(shù)的處理)。
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); //創(chuàng)建委托 d(1); //調用委托,將輸出1 auto d1 = CreateDelegate(&a, &A::Fun1); //創(chuàng)建委托 d1(1, 2.5); //調用委托,將輸出3.5 }
MyDelegate實現(xiàn)的關鍵是內部定義了一個能接受任意類型和個數(shù)參數(shù)的“萬能函數(shù)”:R (T::*m_f)(Args…),正是由于可變模版參數(shù)的特性,所以我們才能夠讓這個m_f接受任意參數(shù)。
4.總結
使用變參模板能夠簡化代碼,正確使用的關鍵是如何展開參數(shù)包,展開參數(shù)包的過程是很精妙的,體現(xiàn)了泛化之美、遞歸之美,正是因為它具有神奇的“魔力”,所以我們可以更泛化的去處理問題,比如用它來消除重復的模版定義,用它來定義一個能接受任意參數(shù)的“萬能函數(shù)”等。其實,可變模版參數(shù)的作用遠不止文中列舉的那些作用,它還可以和其它C++11特性結合起來,比如type_traits、std::tuple等特性,發(fā)揮更加強大的威力。
以上就是詳解C++11 變參模板的詳細內容,更多關于C++11 變參模板的資料請關注腳本之家其它相關文章!
相關文章
VSCode遠程代碼開發(fā)及DNS隧道端口轉發(fā)實現(xiàn)遠程辦公代碼
這篇文章主要介紹了VSCode遠程代碼開發(fā)及DNS隧道端口轉發(fā)實現(xiàn)遠程辦公,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04C++的靜態(tài)聯(lián)編和動態(tài)聯(lián)編
本文闡述了靜態(tài)聯(lián)編和動態(tài)聯(lián)編的概念和區(qū)別,通過具體實例分析了實現(xiàn)動態(tài)聯(lián)編的條件,指出了虛函數(shù)是實現(xiàn)動態(tài)聯(lián)編的基礎。2016-03-03