其實 fold expression 用法很簡單,也是 C++17 的一個語法糖。是糖肯定有他的甜頭,而甜頭在於專門對付 variadic template 和 parameter pack 裡面那一大包東西。
那麼為什麼標題說寫 C++ 像寫數學式一樣呢?因為 fold expression 讓我們用一行接近數列累加的寫法,就能夠完成一堆數字的累加囉。
template<typename... Args>
auto sum(Args... ns)
{
return (0 + ... + ns);
}sum(3, 2, 5, 7, 9); // 26
是不是跟數列加總時中間用 … 代替有幾分神似?好啦我承認大概只有三分像。
上面那行 return
代表的意涵就是 (((0 + ns_0) + ns_1) + ns_2 + ...
,所以就是自動幫你把 parameter pack 參數包裡面的整數都拿出來從左到右加起來了。
在沒有 fold expression 以前,C++14 也是邏輯上可以做到同樣事,只是行數大概是以上的三四倍吧。
所以他只有把 parameter pack 裡的東西全加起來這麼蠢嗎?當然不是,我想聰明的你應該可以發現
+
可以換成大多數 C++ 的二元運算子,+ — * / << >> && || , . ->
族繁不及備載。0
當然可以換成任意其它的整數,就是一個初始值。- 如果 … 換在
ns
的右邊呢?當然就是從ns
這參數包的後面加回來前面,在這個例子裡面結果完全沒差啦,不過針對那些有順序性,不具備交換律的運算子顯然就不一樣了。
最後和 fold expression 比較不那麼直接相關的是,parameter pack Args
也可以不用是一堆 int
,可以是別的型別,只要你傳進來的型別都支援 operator+()
那都行。
繁瑣一點的細節
同學們仔細看上面的 fold expression (0 + ... + ns)
應該會發現左右有括號,這括號是 C++17 中規定必須加上的,也是 fold expression 的一部份。不然編譯器認不得。
再來就是你說有時候我懶得寫初始值,你也可以不要寫,就直接寫 (... + ns)
就好。這叫做 unary fold;相對地,前面有初始值的就叫 binary fold。
所以點點點在左邊和右邊也有差,點在左邊的叫 left fold,點在右邊的就叫 right fold。
(... op args)
: unary left fold(args op ...)
: unary right fold(Init op ... op args)
: binary left fold(args op ... op Init)
: binary right fold
Unary 跟 binary 的區別應該很好記,比較難記的是運算順序,就看 … 在參數包的左邊還是右邊,計算就是先從左邊或右邊開始算。
應用:結合一般表達式
上面說可以把 +
換成別的二元運算子,我們就拿 <<
來試試。同學們應該知道普通的意思就是位元的左移。但 C++ STL 把他發揚光大變成 std::cout
標示資訊流的方向。我們可以利用 std::cout
和 <<
來做一些有趣的事。
印出所有參數
比如把 std::cout
這個物件當作 fold expression 的初始值,然後運算子選用 <<
,參數包一樣是一堆 std::string
,那麼我們不就可以把一堆字串接起來再印出來了嗎?然後最後還想要印個換行。
template<typename ...Args>
void print_sum(Args ...args)
{
(std::cout << ... << args) << std::endl;
}print_sum("abc", "def"); // abcdef
這個例子就體現出 fold expression 為什麼要加上括號了。不然整個混在一起不知道誰才是初始值。這是一個初始值是 std::cout
,然後針對 <<
接在一起的 binary left fold expression。
把所有參數推進去一個 std::vector
跟 parameter pack 的 pack expansion 一樣,我們的 pack ns 可以摻雜在任何表示式,包含函式的呼叫,最後再以 … 加上 ( ) 一口氣展開。
template<typename... Is>
void push_vec(std::vector<std::common_type_t<Is...>> &v, Is... ns)
{
(v.push_back(ns), ...);
}std::vector<int> v{1, 2, 3};
push_vec(v, 1, 2, 3);for (auto &i : v) {
std::cout << i << ' ';
}// 1 2 3 1 2 3
裡面的 std::common_type_t<Is…>
代表參數包 Is
所有成員的共同能被轉換的型別,你開心只寫個 int
也可以。
總之上面意思就是對每一個 ns
中的參數都丟進去 v.push_back()
然後以 ,
作為二元運算子展開。意思其實就相當於把參數 1, 2, 3
展開成三個 v.push_back
指令中間用 ,
隔開而已。
v.push_back(1), v.push_back(2), v.push_back(3);
講完了,用起來真的很簡單,不是什麼高深的語法程式語言設計哲學的東西,就是寫起來很簡潔很爽而已。再搭配 template meta programming 樣版元編程可以做出更多看起來更爽但實際上沒太大鳥用的東西 lol。