天天看點

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

c++11的新特性--可變模版參數(variadic templates)是c++11新增的最強大的特性之一,它對參數進行了高度泛化,它能表示0到任意個數、任意類型的參數。相比c++98/03,類模版和函數模版中隻能含固定數量的模版參數,可變模版參數無疑是一個巨大的改進。然而由于可變模版參數比較抽象,使用起來需要一定的技巧,是以它也是c++11中最難了解和掌握的特性之一。雖然掌握可變模版參數有一定難度,但是它卻是c++11中最有意思的一個特性,本文希望帶領讀者由淺入深的認識和掌握這一特性,同時也會通過一些執行個體來展示可變參數模版的一些用法。

可變參數模闆和普通模闆的語義是一樣的,隻是寫法上稍有差別,聲明可變參數模闆時需要在typename或class後面帶上省略号“...”。比如我們常常這樣聲明一個可變模版參數:template<typename...>或者template<class...>,一個典型的可變模版參數的定義是這樣的:

  上面的可變模版參數的定義當中,省略号的作用有兩個:

1.聲明一個參數包t... args,這個參數包中可以包含0到任意個模闆參數;

2.在模闆定義的右邊,可以将參數包展開成一個一個獨立的參數。

  上面的參數args前面有省略号,是以它就是一個可變模版參數,我們把帶省略号的參數稱為“參數包”,它裡面包含了0到n(n>=0)個模版參數。我們無法直接擷取參數包args中的每個參數的,隻能通過展開參數包的方式來擷取參數包中的每個參數,這是使用可變模版參數的一個主要特點,也是最大的難點,即如何展開可變模版參數。

  可變模版參數和普通的模版參數語義是一緻的,是以可以應用于函數和類,即可變模版參數函數和可變模版參數類,然而,模版函數不支援偏特化,是以可變模版參數函數和可變模版參數類展開可變模版參數的方法還不盡相同,下面我們來分别看看他們展開可變模版參數的方法。

一個簡單的可變模版參數函數:

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

上面的例子中,f()沒有傳入參數,是以參數包為空,輸出的size為0,後面兩次調用分别傳入兩個和三個參數,故輸出的size分别為2和3。由于可變模版參數的類型和個數是不固定的,是以我們可以傳任意類型和個數的參數給函數f。這個例子隻是簡單的将可變模版參數的個數列印出來,如果我們需要将參數包中的每個參數列印出來的話就需要通過一些方法了。展開可變模版參數函數的方法一般有兩種:一種是通過遞歸函數來展開參數包,另外一種是通過逗号表達式來展開參數包。下面來看看如何用這兩種方法來展開參數包。

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

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

上例會輸出每一個參數,直到為空時輸出empty。展開參數包的函數有兩個,一個是遞歸函數,另外一個是遞歸終止函數,參數包args...在展開的過程中遞歸調用自己,每調用一次參數包中的參數就會少一個,直到所有的參數都展開為止,當沒有參數時,則調用非模闆函數print終止遞歸過程。

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

上面的遞歸終止函數還可以寫成這樣:

修改遞歸終止函數後,上例中的調用過程是這樣的:

當參數包展開到最後一個參數時遞歸為止。再看一個通過可變模版參數求和的例子:

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

sum在展開參數包的過程中将各個參數相加求和,參數的展開方式和前面的列印參數包的方式是一樣的。

遞歸函數展開參數包是一種标準做法,也比較好了解,但也有一個缺點,就是必須要一個重載的遞歸終止函數,即必須要有一個同名的終止函數來終止遞歸,這樣可能會感覺稍有不便。有沒有一種更簡單的方式呢?其實還有一種方法可以不通過遞歸方式來展開參數包,這種方式需要借助逗号表達式和初始化清單。比如前面print的例子可以改成這樣:

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

這個例子将分别列印出1,2,3,4四個數字。這種展開參數包的方式,不需要通過遞歸終止函數,是直接在expand函數體中展開的, printarg不是一個遞歸終止函數,隻是一個處理參數包中每一個參數的函數。這種就地展開參數包的方式實作的關鍵是逗号表達式。我們知道逗号表達式會按順序執行逗号前面的表達式,比如:

這個表達式會按順序執行: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表達式了,進而可以少寫一個遞歸終止函數了,具體代碼如下:

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

上面的例子将列印出每個參數,這裡如果再使用c++14的新特性泛型lambda表達式的話,可以寫更泛化的lambda表達式了:

可變參數模闆類是一個帶可變模闆參數的模闆類,比如c++11中的元祖std::tuple就是一個可變模闆類,它的定義如下:

這個可變參數模闆類可以攜帶任意類型任意個數的模闆參數:

可變參數模闆的模闆參數個數可以為0個,是以下面的定義也是也是合法的:

可變參數模闆類的參數包展開的方式和可變參數模闆函數的展開方式不同,可變參數模闆類的參數包展開需要通過模闆特化和繼承方式去展開,展開方式比可變參數模闆函數要複雜。下面我們來看一下展開可變模版參數類中的參數包的方法。

可變參數模闆類的展開一般需要定義兩到三個類,包括類聲明和偏特化的模闆類。如下方式定義了一個基本的可變參數模闆類:

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

  這個sum類的作用是在編譯期計算出參數包中參數類型的size之和,通過sum<int,double,short>::value就可以擷取這3個類型的size之和為14。這是一個簡單的通過可變參數模闆類計算的例子,可以看到一個基本的可變參數模闆應用類由三部分組成,第一部分是:

它是前向聲明,聲明這個sum類是一個可變參數模闆類;第二部分是類的定義:

它定義了一個部分展開的可變模參數模闆類,告訴編譯器如何遞歸展開參數包。第三部分是特化的遞歸終止類:

通過這個特化的類來終止遞歸:

這個前向聲明要求sum的模闆參數至少有一個,因為可變參數模闆中的模闆參數可以有0個,有時候0個模闆參數沒有意義,就可以通過上面的聲明方式來限定模闆參數不能為0個。上面的這種三段式的定義也可以改為兩段式的,可以将前向聲明去掉,這樣定義:

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

上面的方式隻要一個基本的模闆類定義和一個特化的終止函數就行了,而且限定了模闆參數至少有一個。

遞歸終止模闆類可以有多種寫法,比如上例的遞歸終止模闆類還可以這樣寫:

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

在展開到最後兩個參數時終止。

還可以在展開到0個參數時終止:

還可以使用std::integral_constant來消除枚舉定義value。利用std::integral_constant可以獲得編譯期常量的特性,可以将前面的sum例子改為這樣:

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

還可以通過繼承方式來展開參數包,比如下面的例子就是通過繼承的方式去展開參數包:

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

其中makeindexes的作用是為了生成一個可變參數模闆類的整數序列,最終輸出的類型是:struct indexseq<0,1,2>。

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

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

通過不斷的繼承遞歸調用,最終得到整型序列indexseq<0, 1, 2>。

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

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

我們看到了如何利用遞歸以及偏特化等方法來展開可變模版參數,那麼實際當中我們會怎麼去使用它呢?我們可以用可變模版參數來消除一些重複的代碼以及實作一些進階功能,下面我們來看看可變模版參數的一些應用。

c++11之前如果要寫一個泛化的工廠函數,這個工廠函數能接受任意類型的入參,并且參數個數要能滿足大部分的應用需求的話,我們不得不定義很多重複的模版定義,比如下面的代碼:

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

 view code

可以看到這個泛型工廠函數存在大量的重複的模闆定義,并且限定了模闆參數。用可變模闆參數可以消除重複,同時去掉參數個數的限制,代碼很簡潔, 通過可變參數模版優化後的工廠函數如下:

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

c++中沒有類似c#的委托,我們可以借助可變模版參數來實作一個。c#中的委托的基本用法是這樣的:

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

c#中的委托的使用需要先定義一個委托類型,這個委托類型不能泛化,即委托類型一旦聲明之後就不能再用來接受其它類型的函數了,比如這樣用:

這裡不能泛化的原因是聲明委托類型的時候就限定了參數類型和個數,在c++11裡不存在這個問題了,因為有了可變模版參數,它就代表了任意類型和個數的參數了,下面讓我們來看一下如何實作一個功能更加泛化的c++版本的委托(這裡為了簡單起見隻處理成員函數的情況,并且忽略const、volatile和const volatile成員函數的處理)。

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

mydelegate實作的關鍵是内部定義了一個能接受任意類型和個數參數的“萬能函數”:r  (t::*m_f)(args...),正是由于可變模版參數的特性,是以我們才能夠讓這個m_f接受任意參數。

使用可變模版參數的這些技巧相信讀者看了會有耳目一新之感,使用可變模版參數的關鍵是如何展開參數包,展開參數包的過程是很精妙的,展現了泛化之美、遞歸之美,正是因為它具有神奇的“魔力”,是以我們可以更泛化的去處理問題,比如用它來消除重複的模版定義,用它來定義一個能接受任意參數的“萬能函數”等。其實,可變模版參數的作用遠不止文中列舉的那些作用,它還可以和其它c++11特性結合起來,比如type_traits、std::tuple等特性,發揮更加強大的威力,将在後面模闆元程式設計的應用中介紹。

後記:本文的内容主要來自于我在公司内部教育訓練的一次課程,因為很多人對c++11可變模闆參數搞不清或者了解得不深入,是以我覺得有必要拿出來分享一下,讓更多的人看到,就整理了一下發到程式員雜志了,我相信讀者看完之後對可變模闆參數會有全面深入的了解。

一點夢想:盡自己一份力,讓c++的世界變得更美好!

from:https://www.cnblogs.com/qicosmos/p/4325949.html

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