1. 程式人生 > >使用C++11變長引數模板 處理任意長度、型別之引數例項

使用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);//非法
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';
}

方法二:

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。

轉載請註明出處