C++11 的「…」:Parameter Pack

| | 0 Comments| 14:13
Categories:

這篇文章,是當 Heresy 想研究「metaprogramming」(維基百科)的時候,看《Practical C++ metaprogramming》這本書(C++11 在 template 這邊所新增的東西,主要分成「template parameter pack」和「function parameter pack」兩部分;他最明顯的特徵,就是程式碼裡面會多出一堆「…」了~ XD

這部分的文件,可以參考 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 相比,只是 classType 之間多了「」。

當然,這邊只是簡單示意,裡面並沒有真的去儲存東西。

而在定義了這樣的 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++ metaprogramming》的例子或許會比較有感覺一點。

在該書中,他這部分的程式主要是想要把一個傳統函式的所有引數,轉換成 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);
}

這樣的形式。而其中,他收到的 E1E2E3,就是 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);
};

而他的結果會是編譯階段決定的,所以可以用在不少地方。

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *