LoginSignup
10

More than 5 years have passed since last update.

Boost.Rangeをパイプライン記法のままコンテナに変換

Posted at

全てをパイプライン風に書きたい

たとえば、1以上10以下の整数列……から奇数を取りだして……それぞれ二乗した……列が欲しいとき、Scalaのような関数型言語だとメソッドチェーンでさらっと書けます。やりたい気持ちをそのまま書けるので分かりやすいですね。

val vec = (1 to 10).
  filter(i => i % 2 != 0).
  map(i => i * i).
  toVector

print(vec.mkString(",")) // 1,9,25,49,81

C++でもBoost.Rangeを使うと、

auto range = boost::irange(1, 10)
  | boost::adaptors::filtered([](int i){ return i % 2 != 0; })
  | boost::adaptors::transformed([](int i){ return i * i; })

for (int i : range) std::cout << i << ","; //  1,9,25,49,81,

とパイプライン風に書けます。初めて見ると本当にC++か疑ってしまうような記法ですが、黒魔術もといメタプログラミングを駆使するBoostなので何が出来ても不思議ではありませんね :scream:

さて、すぐに処理しおえるのであればBoost.Rangeの世界で完結しますが、いったん評価してしまってコンテナに格納したい時があります。そういう時は、コンストラクタ使ったり、

std::vector<int> vec(std::begin(range), std::end(range));

コピー関数使ったり、

std::vector<int> vec;
boost::copy(range, std::back_inserter(vec));

する必要があります。どうせならコンテナに変換するところまでパイプライン風に書きたいものです。

Boost + Oven = :heart_eyes:

高橋晶さんのBoost.Range拡張ライブラリを使うと、コンテナ変換もできるそうです。すごい!
これは現在Boost Formal Review Scheduleに載っていて、マージ待ちのようです。
boost単体で使えるようになると、とても嬉しいですね。

std::vector<int> vec = boost::irange(1, 10)
  | boost::adaptors::filtered([](int i){ return i % 2 != 0; })
  | boost::adaptors::transformed([](int i){ return i * i; })
  | boost::as_container; // !?

boost::as_containerは、PStade.Ovenというライブラリのoven::copiedを移植したものだそうです。
ちなみにPStade.Ovenの作者 @okomok さんは、Scalaのメタプログラミングライブラリsingなども作っています。すごい……!

関連記事:型に数値を埋めこんでみよう

自前で作ってみる

実はboost::as_containerを見つける前に、似たようなものを試行錯誤して作ってみたのですが、参考にした記事のうち「C++ で拡張メソッドできるよ」「initializer_list(仮)の前準備 container_convertor」が高橋晶さんのブログだったので、先人の軌跡をなぞっていただけでした :open_mouth:
でも、せっかくなので、自前で作った方も書いておきます。

まずboost::adaptorsの実装を覗きながら、std::vectorに変換するto_vecを作りました。
to_vecは、自前のoperator |を呼びだすの単なるタグなのですが、これはtag dispatchingと呼ばれる手法とのことです。

#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/irange.hpp>
#include <iterator>
#include <vector>

// 自前のoperator|をオーバーロードで呼びだすためのタグ
struct ToVector {};
ToVector to_vec = ToVector();

template<
  typename Range, // 制約:value_typeが定義されていること、std::beginとstd::end呼べること
  typename T = typename Range::value_type
>
std::vector<T> operator | (
  const Range& range,
  ToVector // タグ
) {
  return std::vector<T>(std::begin(range), std::end(range));
}

int main() {
  std::vector<int> vec = boost::irange(1, 10)
    | boost::adaptors::filtered([](int n){ return n % 2 != 0; })
    | boost::adaptors::transformed([](int n){ return n * n; })
    | to_vec; // できた!

  return 0;
}

同様に、合計したり平均取ったりするreduce関数のタグも作れます。

さて、これでboost::as_containerと同等の機能が実現……できていないですね。
boost::as_containerの凄いところは、変換する具体的なコンテナをstd::vectorに限らずstd::liststd::dequeなど代入先の型で選べるところです。
これを実現するために、暗黙の型変換を行う補助クラスを作りました。

#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/irange.hpp>
#include <iterator>
#include <vector>

// 暗黙の型変換クラス
template <typename Range>
class RangeConverter {
  const Range& range_;

public:
  RangeConverter(const Range& range): range_(range) {}

  // 暗黙の型変換関数
  template <
    typename Target // 制約:先頭と末尾のイテレーターによるコンスラクタがあること
  >
  operator Target() const {
    return Target(std::begin(range_), std::end(range_));
  }
};

struct ToContainer {};
ToContainer to_cont = ToContainer();

template<
  typename Range,
  typename T = typename Range::value_type
>
RangeConverter<Range> operator | (
  const Range& range,
  ToContainer
) {
  return RangeConverter<Range>(range); // 具体的なコンテナへの変換はRangeConverterに委ねる
}

int main() {
  std::vector<int> vec = boost::irange(1, 10)
    | boost::adaptors::filtered([](int n){ return n % 2 != 0; })
    | boost::adaptors::transformed([](int n){ return n * n; })
    | to_cont; // できた……!

  return 0;
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10