之前 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 有:
+
、-
、*
、/
、%
、^
、&
、|
、=
、<
、>
、<<
、>>
、+=
、-=
、*=
、/=
、%=
、^=
、&=
、|=
、<<=
、>>=
、==
、!=
、<=
、>=
、&&
、||
、,
、.*
、->*
。