這篇文章,是當 Heresy 想研究「me
這部分的文件,可以參考 cppreference,Heresy 這邊算是簡單紀錄、整理一下。
Template Parameter Pack
「Template parameter pack」的功用,是允許開法者在設計 template 時,接受零個或多個 template arguments;而當一個 template 有用到「Template parameter pack」的話,則可以被稱為「variadic template」(維基百科的翻譯是「可變參數模板」、連結)。
例如說,當我們想時做一個類似 std::tuple 這樣,可以把任意型別、任意數量的物件包起來的型別時,透過 template parameter pack 的概念,可能可以實作成:
template<class... Types> struct Tuple {};
這樣的形式。基本上和一般的 template 相比,只是 class 和 Type 之間多了「…」。
當然,這邊只是簡單示意,裡面並沒有真的去儲存東西。
而在定義了這樣的 template structure 後,就可以像下面這樣使用了:
Tuple<> t0; Tuple<int> t1; Tuple<int, float> t2;
也就是和前面提到的一樣,這個 Tuple 的結構,允許零個、或多個不同的型別作為他的 template arguments。
而只要把儲存機制也寫出來,他就可以和 std::tuple 一樣,當作一個任意數量、任一類型的容器來使用了~
Function Parameter Pack
Function parameter pack 的部分,則有點類似 C 語言的「variadic functions」(參考),他可以讓一個函式接受玲個或多個不同型別的資料作為函式的引數。
像下面 f() 這個函式,就是一個最簡單的 function parameter pack 的寫法。
template<class ... Types> void f(Types ... args);
這邊在 template 的寫法和 template parameter pack 時基本上相同,但是在函式的引數定義時,則是寫成「Type … args」這樣的型式,其中 args 就是函式接收到的引數清單。
而在呼叫 f() 的時候,就會類似使用 C 的 printf() 一樣,可以在後面給他不限數量、不限型別的資料(當然,printf() 有限制第一個一定是要是 C 的字串)。
像下面這些呼叫都是合法的:
f(); f(1); f(2, 1.0);
Pack Expansion:基本
至於這樣的東西要怎麼用呢?大致來說,最簡單的用法,就是直接把「…」從中間搬到變數的後面去。
這部分在 cppreference 也有給一些範例。不過在 template parameter pack 的部分,Heresy 覺得用《Practical C++ me
在該書中,他這部分的程式主要是想要把一個傳統函式的所有引數,轉換成 std::tuple 的形式;而他的寫法如下(有略為修改過):
template <typename F> struct make_tuple_of_params; template <typename Ret, typename... Args> struct make_tuple_of_params<Ret(*)(Args...)> { using type = std::tuple<Args...>; }; template <typename F> auto magic_wand(F f) { return make_tuple_of_params<F>::type(); }
這邊主要是定義出 make_tuple_of_params 這個 template 的結構,並在他裡面定義一個「type」的型別別名,讓它等同於「std::tuple<Args…>」。
而之後的 magic_wand() 則是可以接受一個函式,並回傳一個 std::tuple,裡面的型別是這個函式的引數。
比如說當定義一個函式:
void test_func(int in1, float in2, char out2){}
並把 test_func 傳進 magic_wand() 的話,程式可以寫成:
auto tPara = magic_wand(test_func);
而此時,tPara 的型別就會是「std::tuple<int,float,char>」。
實際上,當在撰寫出上面這行程式、進行編譯時,make_tuple_of_params 裡的「Args…」就會被展開成「int,float,char」,所以
using type = std::tuple<Args...>;
這行實際上就會變成
using type = std::tuple<int, float, char>;
這樣的樣子,然後在編譯階段產生對應這個類別的程式碼。
所以 tPara 的型別才會是「std::tuple<int,float,char>」。
而在 function parameter pack 的部分,基本的寫法也是相同的。
假設現在有兩個函式是寫成下面的樣子的話:
template<class ...Us> void f(Us... pargs) {} template<class ...Ts> void g(Ts... args) { f(args...); }
那當呼叫
g(1, 0.2, "a");
的時候,這兩個函式基本上會變成
void f(int E1, double E2, const char* E3){} void g(int E1, double E2, const char* E3) { f(E1, E2, E3); }
這樣的形式。而其中,他收到的 E1、E2、E3,就是 1、0.2 和 "a";而 g() 裡面的 f(args…) 也會被展開成 f(E1, E2, E3)。
寫一個 printf()
上面講的基本上都還只算是概念,真要用的時候,可以怎麼用呢?在 cppreference 的最後,提供了一個簡易版的 printf() 的範例、tprintf();他的原始碼如下:
#include <iostream> void tprintf(const char* format) { std::cout << format; } template<typename T, typename... Targs> void tprintf(const char* format, T value, Targs... Fargs) { for (; *format != ''; format++) { if (*format == '%') { std::cout << value; tprintf(format + 1, Fargs...); return; } std::cout << *format; } } int main() { tprintf("% world% %n", "Hello", '!', 123); return 0; }
這邊的設計,基本上是把 tprintf() 收到的第一個引數、format 這個字串中的「%」依序用後面的其他引數來替換。
這邊的第一個 tprintf() 是最簡單的狀況,也就是沒有其他引數時,直接將 format 輸出。
而第二個 tprintf() 則是會把 format 以外第一個引數(value)拿出來,當掃到 format 中的「%」時,就改輸出 value,然後再去呼叫 tprintf(),並把 format(這時候的字串已經變短了)和少了一項的 Frags… 傳進去。
實際上乎叫的堆疊大概會是這樣的狀況:
tprintf("% world% %n", "Hello", '!', 123); tprintf(" world% %n", '!', 123); tprintf(" %n", 123); tprintf(" n");
而這樣遞迴式第一個一個把不定數量的引數處理掉,應該也算是最普遍的用法了。
Pack Expansion & Pattern
上面所寫的 Pack Expansion 基本上都是最基本的狀況,也就是直接展開。而實際上,C++11 在 Pack Expansion 這部分,其實還給了相當大的彈性,讓開發者可以再展開的時候,去指定 pattern、產生一些有趣的東西。
像是前面 f()、g() 兩個函式的例子,如果改寫成:
template<class ...Us> void f(Us... pargs) {} template<class ...Ts> void g(Ts... args) { f(&args...); }
的話,那在呼叫
g(1, 0.2, "a");
的時候,則會變成下面的形式:
void f(int* E1, double* E2, const char** E3){} void g(int E1, double E2, const char* E3) { f(&E1, &E2, &E3); }
也就是在展開「&args…」的時候,他會把 args 的每一項都加上 &、再傳遞下去!也因此,之前本來和 Ts 相同的 Us,在修改之後也就不一樣了。
而在 cppreference 這邊,也給了一些例子,可以參考:
f(n, ++args...); // expands to f(n, ++E1, ++E2, ++E3); f(++args..., n); // expands to f(++E1, ++E2, ++E3, n); f(const_cast<const Args*>(&args)...); // f(const_cast<const E1*>(&X1), // const_cast<const E2*>(&X2), // const_cast<const E3*>(&X3)) f(h(args...) + args...); // expands to f(h(E1,E2,E3) + E1, h(E1,E2,E3) + E2, h(E1,E2,E3) + E3)
可以看到,其實玩法還滿多、滿雜的…
而如果用在 Template Parameter Pack 上呢?cppreference 也給了一個有趣的 zip.with 的例子:
template<typename...> struct Tuple {}; template<typename T1, typename T2> struct Pair {}; template<class ...Args1> struct zip { template<class ...Args2> struct with { typedef Tuple<Pair<Args1, Args2>...> type; }; };
這東西有什麼用呢?基本上,他可以把兩組型別依序、一對一遞產生出 Pair、然後放到 Tuple 裡面。
使用上,可以寫成下面的樣子:
typedef zip<short, int>::with<unsigned short, unsigned>::type T1;
這樣的話,T1 的型別就是 Tuple<Pair<short, unsigned short>, Pair<int, unsigned>>。
另外,Template Parameter Pack 也可以用在多重繼承、初始化上:
template<class... Mixins> class X : public Mixins... { public: X(const Mixins&... mixins) : Mixins(mixins)... { } };
針對「Parameter Pack」的部分,大概就先記錄到這邊了。沒意外的話,下一篇應該還會整理一下 C++14 的 std::integer_sequence、以及他們一起使用的應用。
但是老實說,雖然很認真看整理了這篇,但是老實說…Heresy 自己好像還不太知道這東西到底該怎麼用? @@
尤其是在 Template parameter pack 的部分,感覺上要能妥善利用,好像很不容易?像是以上面的 Tuple 的例子來說,真要實作到可以存取資料,其實也頗複雜啊~(可以參考《Variadic templates in C++》這篇文章)
總之,之後再繼續看看到底能怎麼玩吧。
另外,最後再補充一下,如果想要知道 Parameter Pack 的大小的話,在 C++11 也有提供一個 sizeof…() 的函式(參考)來取得他的大小。
template<class... Types> struct Tuple { static const std::size_t size = sizeof...(Types); };
而他的結果會是編譯階段決定的,所以可以用在不少地方。