C++でパイプを実現する方法 - それと後置クロージャへの応用

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

演算子オーバロードで実現します。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);  // リダイレクト風
広告