C++でパイプラインを実現する方法を紹介します。次のような|
記号による記述が可能となります。
std::vector<int> v = {1, 2, 3};
// Before
v = map(v, [](int v) { return v * 2; });
// After
v = v | map([](int v) { return v * 2; });
for (auto i : v | map([](int v) { return v * 2; })
| filter([](int v) { return v < 5; })) {
printf("%d,", i);
}
今回は用途やスタイルに合わせて複数の方法を紹介していきます。
目次
- パイプ処理の実現1(
list | map(f)
) - パイプ処理の実現2(
map(list) | f
) - 後置クロージャの実現(
map(list) ^f
)
パイプ処理の実現1
演算子オーバロードで実現します。map関数やfilter関数は自作する必要があります。今回は例としてmap関数を実装します。
template<class F> struct Callable {
F f;
template<class T> auto operator()(const T& v) const { return f(v); }
};
template<class T, class F> auto operator|(const T& v, const Callable<F>& f) { return f(v); }
template<class F> auto map(F f) {
auto g = [=](const auto& c) {
std::remove_cv_t<std::remove_reference_t<decltype(c)>> r;
return std::transform(c.begin(), c.end(), std::back_inserter(r), f), r;
};
return Callable<decltype(g)>{g};
}
int main() {
std::vector<int> v = {1, 2, 3};
v = v | map([](int v) { return v * 2; });
// v == {2, 4, 6}
v = v | map([](int v) { return v * 2; })
| map([](int v) { return v * 2; });
// v == {8, 16, 24}
std::string("Abc") | map(toupper); // "ABC"
std::string("Abc") | map([](auto c) { return tolower(c); }); // "abc"
}
マップ処理は、map関数の内部でCallableオブジェクトへと束縛されます。束縛された実際のマップ処理はCallableを引数として受け取る|
演算子が実行します。
パイプ処理の実現2
次のような記法を実現します。
using namespace std::literals::string_literals;
map("Abc"s) | [](auto c) { return tolower(c); }; // "abc"
map("Abc"s) | toupper; // "ABC"
先程の# パイプ処理の実現1とやっていることはほとんど変わりません。
template<class F> struct Callable {
F f;
template<class T> auto operator()(const T& v) const { return f(v); }
};
template<class T, class F> auto operator|(const Callable<F>& f, const T& v) { return f(v); }
template<class Container> auto map(const Container& c) {
auto f = [&](auto g) {
Container r;
return std::transform(c.begin(), c.end(), std::back_inserter(r), g), r;
};
return Callable<decltype(f)>{f};
}
int main() {
std::vector<int> v = {1, 2, 3};
v = map(v) | [](int v) { return v * 2; };
// v == {2, 4, 6}
std::string s = "Abc";
map(s) | toupper; // "ABC"
map(s) | [](auto c) { return tolower(c); }; // "Abc"
}
後置クロージャの実現
先程の# パイプ処理の実現2の応用例です。Rubyの引数リストの末尾ブロック記法や、SwiftのTrailing Closureに相当する記法を実現します。あくまで実験的なテクニックであり、大したメリットや実用性はありません。C++研究にお役立て下さい。
先程の|
演算子と同様に^
演算子のオーバーロードを追加することで実現します。
template<class T, class F> auto operator^(const Callable<F>& f, const T& v) { return f(v); }
以下のような記述が可能となります。
std::vector<int> v = {1, 2, 3};
// w == {2, 4, 6}
auto w = map(v) ^[](int i) {
return i * 2;
};
std::string s = "abc";
s = map(s) ^toupper; // "ABC"
// s = (filter(s) ^isalpha);
// ブロック(Blocks)にも対応(Apple LLVM Clang拡張)
map(s) ^^(char c) { return c + 1; };
// その他演算子の活用例
s = map(s) >> toupper; // ストリーム風
s = (map(s), toupper); // Lisp風
s = map(s)-toupper; // メッセージチェーン風
s = toupper < map(s); // リダイレクト風