潮.C++17 | Fold Expression 寫 C++ 像寫數學式

TJSW
5 min readApr 6, 2019

--

其實 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 裡的東西全加起來這麼蠢嗎?當然不是,我想聰明的你應該可以發現

  1. + 可以換成大多數 C++ 的二元運算子,+ — * / << >> && || , . -> 族繁不及備載。
  2. 0 當然可以換成任意其它的整數,就是一個初始值。
  3. 如果 … 換在 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。

--

--