使用C++11變長參數模板 處理任意長度、類型之參數實例

變長模板、變長參數是依靠C++11新引入的參數包的機制實現的。

一個簡單的例子是std::tuple的聲明:

template <typename... Elements>
class tuple;

這裏的三個點“...”表示這個模板參數是變長的。

有了這個強大的工具,我們可以編寫更加豐富的函數,例如任意類型參數的printf等。由於這個技術還比較新,還沒有見到成熟的用法用例,我把我嘗試的一些結果總結如下,希望對大家有幫助。


1,參數包

考慮到這個知識點很多朋友都不熟悉,首先明確幾個概念:

1,模板參數包(template parameter pack):
它指模板參數位置上的變長參數(可以是類型參數,也可以是非類型參數),例如上面例子中的 Elements。

2,函數參數包(function parameter pack):

它指函數參數位置上的變長參數,例如下面例子中的args,(ARGS是模板參數包):

template <typename ... ARGS>
void fun(ARGS ... args)


在很多情況下它們是密切相關的(例如上面的例子),而且很多概念和用法也都一致,在不引起誤解的情況下,後面我在討論時會將他們合併起來討論,或只討論其中一個(另一個於此相同)。

注意:

模板參數包本身在模板推導過程中被認爲是一個特殊的類型(函數參數包被認爲是一個特殊類型的參數)。

一個包可以打包任意多數量的參數(包含0個)。

有一個新的運算符:sizeof...(T) 可以用來獲知參數包中打包了幾個參數,注意不是參數所佔的字節數之和。

一般情況下參數包必須在最後面,例如:

template <typename T, typename ... Args>
void fun(T t,Args ... args);//合法

template <typename ... Args, typename T>
void fun(Args ... args,T t);//非法

有人說了,我向來是二班的,什麼奇葩情況都能遇見。那麼二般情況請參見: http://stackoverflow.com/questions/4706677/partial-template-specialization-with-multiple-template-parameter-packs 和 http://stackoverflow.com/questions/9831501/how-can-i-have-multiple-parameter-packs-in-a-variadic-template(大致如下)。

template < typename... Types1, template <typename...> class T
         , typename... Types2, template <typename...> class V>
void bar(const T<Types1...>&, const V<Types2...>&)
{
  std::cout << sizeof...(Types1) << std::endl;
  std::cout << sizeof...(Types2) << std::endl;
}

調用: 
tuple<int,double> a;
tuple<char,float,long> b;
bar(a,b);
總之就是讓編譯器能夠輕鬆地唯一地確定包到底有多大就可以了。

2,解包 (包展開)

在實際使用時,拿到一個複合而成的包對沒有並沒有什麼用,我們通常需要獲得它裏面內一個元素的內容。解包是把參數包展開爲它所表示的具體內容的動作。

解包時採用“包擴展表達式”,就是包名加上三個點,如“Args...”。

例如:

假設我們有一個模板類Base:

template <typename ... Args>
class D1 : public Base<Args...>{};
或
template <typename ... Args>
class D2 : public Base<Args>...{};
解包用兩種常見的形式:

1,直接解包(上面第一個)

D1<X,Y,Z> 相當於 D1:public Base<X,Y,Z>

2,先參與其他表達式再解包(上面第二個)

D2<X,Y,Z> 相當於 D2: public Base<X>, Base<Y>, Base<Z>


直觀上理解就是在...所在的位置將包含了參數包的表達式展開爲若干個具體形式。


參數包的展開不能無條件地在任何地方使用,這會給編譯器看到的源代碼的結構帶來很大的複雜性。嚴格來說標準規定可以進行參數包展開的有7中情況:1,表達式;2,初始化列表;3,基類描述列表;4,類成員初始化;5,模板參數列表;6,通用屬性列表;7,lambda函數的捕獲列表。

例如下面例子的兩個展開就是非法的:

template <typename T>
void fun_hehe(T t){
    //do something
}

template <typename... T>
void fun(T... t){
    t...;
    fun_hehe(t)...;
}

第一個(t...)非法很好理解,直接並列一堆東西沒有意義嘛。第二個(fun_hehe(t)...)貌似是有意義的,但是一般情況下不能這樣用,需要類似的功能時可以採用下一節介紹的方法2。可以簡單地認爲:不能讓展開之後的表達式成爲一個獨立的語句。

3,函數實例

一個常用的技巧是:利用模板推導機制,每次從參數包裏面取第一個元素,縮短參數包,直到包爲空。
template <typename T>
void fun(const T& t){
	cout << t << '\n';
}

template <typename T, typename ... Args>
void fun(const T& t, Args ... args){
	cout << t << ',';
	fun(args...);//遞歸解決,利用模板推導機制,每次取出第一個,縮短參數包的大小。
}


下面我以打印出一組參數爲例,簡單介紹一下變成參數函數怎麼用。

方法一:

template <typename ... T>
void DummyWrapper(T... t){}

template <class T>
T unpacker(const T& t){
	cout<<','<<t;
	return t;
}

template <typename T, typename... Args>
void write_line(const T& t, const Args& ... data){
	cout << t;
	DummyWrapper(unpacker(data)...); //直接用unpacker(data)...是非法的,(可以認爲直接逗號並列一堆結果沒有意義)
	//所以需要用一個函數包裹一下,就好像這些結果後面還有用
	cout << '\n';
}

雖然寫起來麻煩一點,但是它在運行期的效率比較高(沒有遞歸,順序搞定,DummyWrapper的參數傳遞會被編譯器優化掉),而且編譯期的代價也不是很高(對於相同類型的子元素,unpacker<T>只需要特化出一份即可,但DummyWrapper需要根據參數類型特化很多版本)。

但是這個方法存在一個問題:參數包在展開的時候,是從右(結束)向左(開始)進行的,所以unpacker(data)...所打印出來的東西可能是反序的(gcc的實現會,clang不會)!

所以這種方法對於屏幕輸出這樣要求嚴格順序的操作就不適合了。它的適用範圍更多的是在對順序不敏感的地方。例如將一組序列化後的數據存儲到一個std::map裏去。

感謝@zhx6044 指出:在C++17標準中,可以使用fold expression,更直接地表達,並且確保正序展開:

template <typename T, typename... Args>
void write_line(const T& t, const Args& ... data){
<span style="white-space:pre">	</span>cout<<','<<t;
<span style="white-space:pre">	</span>(unpacker(data), ...);//展開成(((unpacker(data_1), unpacker(data_2)), unpacker(data_3), ... ),unpacker(data_n)
<span style="white-space:pre">	</span>cout<<'\n';


<span style="white-space:pre">	</span>//如果不需要輸出間隔符,後兩行還可以使用下面的簡單形式
<span style="white-space:pre">	</span>//(cout<< ... <<args)<<'\n';
}

詳情參見他的blog: http://blog.csdn.net/zhx6044/article/details/50931003 。

附cppreference上關於fold expression的參考鏈接:http://en.cppreference.com/w/cpp/language/fold

方法二:

template <typename T>
void _write(const T& t){
	cout << t << '\n';
}

template <typename T, typename ... Args>
void _write(const T& t, Args ... args){
	cout << t << ',';
	_write(args...);//遞歸解決,利用模板推導機制,每次取出第一個,縮短參數包的大小。
}

template <typename T, typename... Args>
inline void write_line(const T& t, const Args& ... data){
	_write(t, data...);
}

這種方法思路直觀,書寫便捷,而且可以保證執行順序。但是運行時有遞歸,效率有所下降。編譯時也需要生成不少版本的_write。


原載於http://blog.csdn.net/yanxiangtianji

轉載請註明出處


發佈了52 篇原創文章 · 獲贊 48 · 訪問量 50萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章