全てをパイプライン風に書きたい
たとえば、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なので何が出来ても不思議ではありませんね
さて、すぐに処理しおえるのであれば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 =
高橋晶さんの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」が高橋晶さんのブログだったので、先人の軌跡をなぞっていただけでした
でも、せっかくなので、自前で作った方も書いておきます。
まず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::list
やstd::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;
}