之前 Heresy 曾經寫過一篇《C++11 的「…」:Parameter Pack》,算是簡單介紹了 C++11 新增的 「parameter pack」;這項功能,基本上就是用「...」來處理數量不固定的函式引數、以及 template parameter。
而在當時來說,真的要去處理這些數量不固定的 parameter pack,大概都是得用類似遞迴的方法來寫了。
到了 C++17,為了簡化這邊的開發難度,則是又引進了「fold expressions」的語法、來讓開發者可以更簡單地處理 parameter pack。(參考)
基本語法
這邊基本語法有四個:
(pack op ...):unary right fold(... op pack):unary left fold(pack op ... op init):binary right fold(init op ... op pack):binary left fold
這邊的 pack 就是包含 parameter pack 的一段 expression,在最簡單的狀況下會是之前用代表 parameter pack 的變數。
而 op 則是有支援的 binary operator(二元運算子)、包含常見的加減乘除在內總計 32 個;至於 init 的話,則應該算是這一連串處理時的一個額外的初始值,同樣也是一段 expression。
其中可能要注意的一點是,這邊的 () 是必要的,不可以省略。
而這邊雖然雖然有四種寫法,不過 binary fold 基本上是 unary fold 加上初始值,算是一種延伸寫法了。
至於 ... 放在前面或後面,則是影響處理的順序,如果連續的 op 操作不會受到順序影響的話,其實兩者的結果會完全相同。
以第一個 (pack op ...) 來說,假設 pack 裡面的資料是 { E1, E2, E3, E4 } 的話,那它實際上的運作就會是 ( E1 op ( E2 op ( E3 op E4 ))),也就是從右側開始做處理。
而 (... op pack) 則是順序反過來,變成是 ((( E1 op E2 ) op E3 ) op E4 )。
Binary fold 的部分,則是額外把初始值也拿進來用而已,像是 (pack op ... op init) 就是變成 ( E1 op ( E2 op ( E3 op ( E4 op I ))))。
簡單的範例
而實際寫的話,大概會是像下面這樣:
#include <iostream> template<typename... Args> bool all(Args... args) { return (... && args); } int main() { bool b = all(true, true, true, false); std::cout << b << std::endl; }
在 all() 這個函式裡面,實際運作就是 return ((true && true) && true) && false 了。
如果像是搭配初始值使用的的 binary fold 的話,也可以寫成:
#include <iostream> template<typename... Args> void output(Args... args) { (std::cout << ... << args); } int main() { output(1, 2.5, "Hello"); }
這邊的 std::cout 就是 binary fold 的 init,實際上在運作時會是 ( ( (std::cout << 1) << 2.5 ) << "Hello" )。
Parameter pack 的處理
前面有提到,pack 實際上是包含 parameter pack 的一段 expression,也就是其實是可以做一些額外的處理後,再進行 fold 運算的。
比如說下面這段程式:
#include <iostream> #include <string> template<typename... Args> size_t size_sum(Args... strs) { return (std::string_view(strs).size() + ...); } int main() { std::cout << size_sum("abc", "defg", "x") << "\n"; }
在這個例子裡,size_sum() 這個函式是用來計算多個字串長度的總和的;而裡面的 unary right fold 黃底的部分,就是 pack。
這邊會先把 strs 這個 parameter pack 的每一個元素先轉換成 std::string_view、然後再透過 size() 取得字串的大小,最後再用 + 這個 operator、把得到的字串大小都累加起來。
也由於是允許一個 expression,所以其實使用上的彈性也算滿大的、可以玩出很多功能。
搭配 comma operator
在 C++ 裡面,其實「,」也是一個 binary operator,他兩邊的程式都會去執行,但是會忽視左邊的回傳值、只採用右邊的回傳值。(老實說,個人幾乎沒有特別去注意過他?)
下面是一個簡單的例子:
#include <iostream> int comp(const int v) { std::cout << "compute with " << v << "\n"; return v; } int main() { int v = (comp(1), comp(2), comp(3)); std::cout << "return: " << v << "\n"; }
這邊會去呼叫 comp() 三次,但是只有最後一次的結果會回傳出來,所以最後 v 的值會是 3。
而如果拿「,」來搭配 fold 使用的話,就可以做到類似 for 迴圈、針對 parameter pack 裡面的所有的元素各自去做彼此不相關的運算。
下面就是一個使用的例子:
#include <iostream> #include <vector> template<typename... Args> std::vector<int> build_vec(Args... vals) { std::vector<int> v; (v.push_back(vals), ...); return v; } int main() { auto v = build_vec(1,2,3,4,5); }
這邊就是透過 fold 來把 build_vec() 的所有引數,都丟到 std::vector 裡面。
不過這邊比較特別的,是在搭配「,」的時候,「...」放在前面或後面似乎都不會影響執行的順序?也就是說 unary right fold 和 unary left fold 的結果看來都是一樣的。
改良版的輸出
前面 output() 的例子,其實輸出的結果算是相當不好看的;而如果想要讓輸出結果更漂亮,可以寫成:
#include <iostream> template<typename T> void output_impl(const T& v) { std::cout << ", " << v; } template<typename T1, typename... Args> void output(const T1& v1, Args... args) { std::cout << "[ " << v1; (output_impl(args), ...); std::cout << " ]"; } int main() { output(1, 2.5, "Hello"); }
這邊的 output() 是修改成把第一個引數獨立出來當作 v1 來處理、 剩下的才當作 parameter pack;而在透過 fold 展開的時候,則是呼叫 output_impl() 這個函式,先輸出 ", " 再輸出變數的值、以作為間隔。這樣的結果,會比本來的版本好看許多~
而如果不想另外定義一個函式的話,其實也可以透過 lambda 來簡化:
#include <iostream> template<typename T1, typename... Args> void output(const T1& v1, Args... args) { std::cout << "[ " << v1; ([](const auto& v) {std::cout << ", " << v; }(args), ...); std::cout << " ]"; } int main() { output(1, 2.5, "Hello"); }
這樣寫起來就更單純了。
針對所有元素個別處理
前面的寫法基本上都是靠 parameter pack 來自動展開,不過如果真的要針對個別元素做處理的話,其實也是可以很簡單地透過建立對應的函式來做到的。
例如下面的程式:
#include <iostream> #include <typeinfo> template<typename T> void check_impl(const T& v) { std::cout << typeid(v).name() << ": " << v << "\n"; } template<typename... Args> void check_type(Args... args) { (check_impl(args), ...); } int main() { check_type(1, 2.5, "Hello"); }
這邊的 check_impl() 就是會取得個別的元素、並輸出他的型別名稱以及值;而如果要針對不同型別做不同的處理,就只要實作針對不同型別的 overload 就可以了。
而如果不想要另外定義一個函式話,其實也可以透過 lambda 來實作:
template<typename... Args> void check_type(Args... args) { ([](const auto& v) { std::cout << typeid(v).name() << ": " << v << "\n"; }(args), ...); }
這篇就寫到這裡了。
基本上,fold expression 本質上基本上就是搭配不確定數量引數的 parameter pack 的東西,基本上主要會是編譯階段就要處理的東西。
老實說,這種也算是 Heresy 這邊比較少碰到的東西了。所以這邊雖然花了好些時間來研究、整理,但是實際上到底要用在哪裡?其實還沒有真的想清楚…
附註:
- 支援的 binary operator 有:
+、-、*、/、%、^、&、|、=、<、>、<<、>>、+=、-=、*=、/=、%=、^=、&=、|=、<<=、>>=、==、!=、<=、>=、&&、||、,、.*、->*。
