泛化之美--C++11可變模版參數的妙用

1概述

C++11的新特性--可變模版參數(variadic templates)是C++11新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數。相比C++98/03,類模版和函數模版中只能含固定數量的模版參數,可變模版參數無疑是一個巨大的改進。然而因爲可變模版參數比較抽象,使用起來須要必定的技巧,因此它也是C++11中最難理解和掌握的特性之一。雖然掌握可變模版參數有必定難度,可是它倒是C++11中最有意思的一個特性,本文但願帶領讀者由淺入深的認識和掌握這一特性,同時也會經過一些實例來展現可變參數模版的一些用法。ios

2可變模版參數的展開

可變參數模板和普通模板的語義是同樣的,只是寫法上稍有區別,聲明可變參數模板時須要在typenameclass後面帶上省略號「...」。好比咱們經常這樣聲明一個可變模版參數:template<typename...>或者template<class...>,一個典型的可變模版參數的定義是這樣的:程序員

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

  上面的可變模版參數的定義當中,省略號的做用有兩個:
1.聲明一個參數包T... args,這個參數包中能夠包含0到任意個模板參數;
2.在模板定義的右邊,能夠將參數包展開成一個一個獨立的參數。編程

  上面的參數args前面有省略號,因此它就是一個可變模版參數,咱們把帶省略號的參數稱爲「參數包」,它裏面包含了0到N(N>=0)個模版參數。咱們沒法直接獲取參數包args中的每一個參數的,只能經過展開參數包的方式來獲取參數包中的每一個參數,這是使用可變模版參數的一個主要特色,也是最大的難點,即如何展開可變模版參數。數組

  可變模版參數和普通的模版參數語義是一致的,因此能夠應用於函數和類,便可變模版參數函數和可變模版參數類,然而,模版函數不支持偏特化,因此可變模版參數函數和可變模版參數類展開可變模版參數的方法還不盡相同,下面咱們來分別看看他們展開可變模版參數的方法。ide

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()沒有傳入參數,因此參數包爲空,輸出的size0,後面兩次調用分別傳入兩個和三個參數,故輸出的size分別爲23。因爲可變模版參數的類型和個數是不固定的,因此咱們能夠傳任意類型和個數的參數給函數f。這個例子只是簡單的將可變模版參數的個數打印出來,若是咱們須要將參數包中的每一個參數打印出來的話就須要經過一些方法了。展開可變模版參數函數的方法通常有兩種:一種是經過遞歸函數來展開參數包,另一種是經過逗號表達式來展開參數包。下面來看看如何用這兩種方法來展開參數包。優化

2.1.1遞歸函數方式展開參數包

經過遞歸函數展開參數包,須要提供一個參數包展開的函數和一個遞歸終止函數,遞歸終止函數正是用來終止遞歸的,來看看下面的例子。spa

#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終止遞歸過程。 rest

遞歸調用的過程是這樣的:code

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);
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++版本的委託(這裏爲了簡單起見只處理成員函數的狀況,而且忽略constvolatileconst 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_traitsstd::tuple等特性,發揮更增強大的威力,將在後面模板元編程的應用中介紹

 

本文曾發表於《程序員》2015年2月刊。轉載請註明出處。

後記:本文的內容主要來自於我在公司內部培訓的一次課程,由於不少人對C++11可變模板參數搞不清或者理解得不深刻,因此我以爲有必要拿出來分享一下,讓更多的人看到,就整理了一下發到程序員雜誌了,我相信讀者看完以後對可變模板參數會有全面深刻的瞭解。

相關文章
相關標籤/搜索