摘要訊息 : C++ 17 Proposal N4295《Folding Expressions》導讀
C++ 11 引入了可變參數樣板, 但是不太方便的是, 當我們處理樣板參數包的時候, 通常需要將樣板參數包展開. C++ 在發展的途中, 雖然不斷地加入了新東西, 但是也希望 C++ 的使用者以最高的效率編碼. 因此 C++ 17 引入了折疊表達式, 由編碼器直接處理樣板參數包
我們以求和的函式樣板為例, 一般求和的函式都要求參數可變, 因此我們一般採用可變樣板參數和遞迴的方式來實作 :
template <typename T, typename U>
auto sum(T t, U u) {
return t + u;
}
template <typename T, typename U, typename ...Args>
auto sum(T t, U u, Args ...args) {
return sum(t + u, args...);
}
由於遞迴需要一個結束的標誌, 因此此處我們通常都需要兩個函式. 而 C++ 17 引入折疊表達式之後, 就可以將上述程式碼簡化成以下 :
template <typename ...Args>
auto sum(Args ...args) {
return (args + ...);
}
折疊表達式分為 4 種 :
- 一元右摺疊 :
(pack op ...)
- 一元左摺疊 :
(... op pack)
- 二元右摺疊 :
(pack op ... op param)
- 二元左摺疊 :
(param op ... op pack)
折疊表達式分為一元折疊表達式和二元折疊表達式, 一元折疊表達式表示僅對參數包進行操作, 二元折疊表達式表示除了要對參數包進行操作之外還需要對額外的參數進行操作. op 就是操作符號, 也就是運算子. 支援折疊表達式的運算子有 : +
, -
, *
, /
, %
, ^
, &
, |
, =
, <
, >
, <<
, >>
, +=
, -=
, *=
, /=
, %=
, ^=
, &=
, |=
, <<=
, >>=
, ==
, !=
, <=
, >=
, &&
, ||
, ,
, .*
和 ->*
. pack 表示參數包的名稱, 在上面的程式碼中就是 args
. param 就是額外的參數, 這個額外參數和參數包無關, 更不在參數包之內. 另外, 折疊表達式是屬於一個表達式, 它之間夾雜著空格以及運算子, 所以括弧必不可少. 少了括弧就不再是折疊表達式, 多數時候還會造成編碼錯誤
摺疊除了分為一元和二元之外, 還分左右. 右摺疊表達式表示著折疊表達式的計算將從最右邊開始 :
#include <iostream>
using namespace std;
auto count {0};
class Foo {
private:
int flag;
public:
Foo() : flag {::count++} {}
Foo operator+(const Foo &foo) noexcept {
cout << this->flag << " ";
return {};
}
};
int main(int argc, char *argv[]) {
Foo f[10] {};
#define expand f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]
auto unary_right_fold {[](auto ...args) noexcept -> auto {
(args + ...);
}};
unary_right_fold(expand); //輸出 : 8 7 6 5 4 3 2 1 0
cout << endl;
}
Foo
中多載的 +
運算子輸出左側運算物件中的 flag
, flag
的值取決於 Foo
物件的個數. 由於一元右摺疊表達式是從右側開始計算的, 因此第一個計算必定是 f[8] + f[9]
, 輸出的自然是 f[8]
中的 flag
. 第二個計算是 f[7] + (f[8] + f[9])
, 由於 f[8] + f[9]
會回傳一個新的物件, 但是輸出的對象是 f[7]
, 因此最後的輸出是 7. 接下來都是差不多的流程
了解了這個之後, 我們再來看看二元右摺疊表達式 :
#include <iostream>
using namespace std;
auto count {0};
class Foo {
private:
int flag;
public:
Foo() : flag {::count++} {}
Foo operator+(const Foo &foo) noexcept {
cout << this->flag << " ";
return {};
}
};
int main(int argc, char *argv[]) {
Foo f[10] {};
#define expand f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]
auto binary_right_fold {[](auto ...args) noexcept -> auto {
(args + ... + Foo());
}};
binary_right_fold(expand); //輸出 : 9 8 7 6 5 4 3 2 1 0
cout << endl;
}
像上面一樣, 右摺疊表達式是從最右側開始計算的, 因此第一個計算就是 f[9] + Foo()
, 於是第一個輸出就是 9. 剩下的就和之前差不多了
了解了這些之後, 剩下的兩個都差不多了 :
#include <iostream>
using namespace std;
auto count {0};
class Foo {
private:
int flag;
public:
Foo() : flag {::count++} {}
Foo operator+(const Foo &foo) noexcept {
cout << this->flag << " ";
return {};
}
};
int main(int argc, char *argv[]) {
Foo f[10] {};
#define expand f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]
auto unary_left_fold {[](auto ...args) noexcept -> auto {
(... + args);
}};
unary_left_fold(expand); //輸出 : 0 10 11 12 13 14 15 16 17
cout << endl;
}
左摺疊表達式從最左邊開始計算, 那麼第一個計算是 f[0] + f[1]
, 這個計算會輸出 0. 接下來是 f[0] + f[1]
回傳的物件與 f[2]
的運算. 此時, 這個物件的 flag
必定大於 9, 也就是上面的輸出
同樣的, 二元左摺疊表達式也差不多 :
#include <iostream>
using namespace std;
auto count {0};
class Foo {
private:
int flag;
public:
Foo() : flag {::count++} {}
Foo operator+(const Foo &foo) noexcept {
cout << this->flag << " ";
return {};
}
};
int main(int argc, char *argv[]) {
Foo f[10] {};
#define expand f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8], f[9]
auto binary_left_fold {[](auto ...args) noexcept -> auto {
(Foo() + ... + args);
}};
binary_left_fold(expand); //輸出 : 10 11 12 13 14 15 16 17 18 19
cout << endl;
}
上述程式碼這裡就不再分析了
除此之外, 當面對參數包為空的時候, 折疊表達式也有一些額外的處理. 對於 &&
運算子, 空的參數包的預設值為 true
; 對於 ||
運算子, 那麼空的參數包的預設值為 false
; 對於 ,
運算子, 空的參數包的回傳結果是 void()
Paper 裡還有一些不太符合標準的地方, 此處我就不再講述了, 因為 C++ 17 標準已經發布了
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :