28
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Boost.Rangeってなに

Last updated at Posted at 2016-07-21

Boost.Rangeが流行っている気がするので解説します。それぞれの細かい機能についてはリファレンスを見てね。この記事はBoost.Rangeが分からない人、特に全く知らない向けの記事になっているため、boostが1ミリも分からなくても読める記事になっているつもりです。なお、私がこの記事を書く上で使用したBoostのバージョンは1.60.0です。リファレンスは以下です。URLをちょっといじれば好きなバージョンに飛べると思います。
http://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/
なお、標準ライブラリのアルゴリズムヘッダも参照のこと。
http://en.cppreference.com/w/cpp/algorithm

#Boost.Rangeとは
イテレータ対(iterator pair)というイディオムを応用発展させたboostのライブラリ。イテレータ対というのは、標準ライブラリでも非常によく使われるイディオムです。
iterator pairについてはc++ more idiomsに詳しいです。
https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Iterator_Pair

std::fill(begin(vec), end(vec), // これとか
  0xff);
std::copy(begin(vec), end(vec), // これとか
  std::ostream_iterator<int>(std::cout));
std::list<int> list(begin(vec), end(vec)); // これのこと

コンテナそのものを渡さずに、beginとendというイテレータの対を使ってコンテナではなく"範囲"をやり取りすることで汎用性を上げるイディオムです。でも正直、面倒くさい。

#include <iterator> // 必須

using std::begin; using std::end; // ADLによって呼び出されなければならない為
begin(container); end(container); // containerが2回も登場する

ひどい話ですが、上のように書かなければなりません。STLのアルゴリズム関数は汎用性に優れ、高速で、問題の解決能力も高いですが、ボコボコに罵られることもあります。iterators must goは有名ですね。iterators must goは完全に和訳するのは難しいですが流し読みするのはそう難しくないのでぜひ一読することをおすすめします。

そこでRangeという"範囲"という概念を表すクラスが登場するというのはそんなに革新的な話ではないと思います。beginとendのイテレータのペアを保持するクラスに、emptyやbegin, endなどのメンバ関数を持たせ、コンテナよりも緩いconceptの元で使える、そんな範囲を扱うBoost.Rangeの機能、魅力、そして流行り?のパイプライン記法についても解説したいと思います。

#Boost.Range.Algorithms
Rangeを手に入れてまず思いつくのは、上の面倒くさいアルゴリズム関数の引数をRangeにしてしまうことですね。これだけでも、だいぶ楽ちんになります。Boost.Algorithmとごっちゃにしないように気を付けてください。
機能ごとにヘッダが用意されています。使いたいものだけインクルードしましょう。
http://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/headers/algorithm.html

#include <iterator>
#include <vector>
#include <boost/range/algorithm/copy.hpp>

int main()
{
  std::vector<int> vec { /* initialize */ };
  boost::copy(vec, std::ostream_iterator<int>(std::cout));
}

はい。言いたいことは分かります。ヘッダ名が長い。コンパイル時間も長い気がする。ていうかこのくらい標準でできなかったことが疑問。いえいえ、まだ触りですから、まだまだ序の口です。これでも結構楽になったと思いますけどね。さあ、次行きましょう。

#Boost.Range.Utilities
Rangeが範囲を表すなら、何もbeginとendのペアじゃなくてもいいよね。begin+2 ~ end-8 の範囲だって範囲じゃん。そんなあなたの乙女心をわしづかみにするのがこのBoost.Range.Utilitiesです。
Utilitiesには2つのクラス、すなわちiterator_rangesub_rangeの2つが用意されています。
iterator_rangeはイテレータの型を指定して、そのイテレータの範囲を示すクラスです。sub_rangeはコンテナの型を指定して、そのコンテナの一部の範囲を示すクラスです。便利の為にテンプレート引数の違うこの二つが用意されていますが、実際にはこれらは同じ実装になっています。

#include <boost/range/iterator_range.hpp>
#include <boost/range/sub_range.hpp>
#include <vector>

int main()
{
  std::vector<int> vec { /* initialize */ };
  boost::sub_range<std::vector<char>> sub(vec); // vecの最初から最後まで
  boost::sub_range<std::vector<char>> sub2(begin(vec)+2, begin(vec)+10); // vecの2番目から9番目までを表す
  boost::iterator_range<std::istream_iterator<int>> sub3(
      std::istream_iterator<int>(std::cin),
      std::istream_iterator<int>()); // イテレータ対が基になっているから、こういう応用も効く
}

#Boost.Range.Adaptors
これが本題でしょうか。かなり応用的な話になってしまいます。このライブラリはC++でパイプライン記法のものが使えるようになるライブラリで、かなり黒魔術の様相を呈した見た目ではありますがとっても実用的な形になっています。しかし出入力ストリームと同じように演算子のオーバーロードを悪用しています。
これもヘッダがそれぞれ用意されているので、自分が使うものをインクルードしましょう。
http://www.boost.org/doc/libs/1_60_0/libs/range/doc/html/range/headers/adaptors.html

boost::irangestd::iotaのrange版のようなもので、指定した数値から数値までのrangeを作成してくれる関数です。

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

int main()
{
  // 適当な数列を作ってみる。
  boost::copy(
    boost::irange(0, 100) // 0~99の要素のrangeを作成; 0, 1, 2, 3, ..., 99
      | boost::adaptors::strided(2) // 要素を1つ飛ばしにする; 0, 2, 4, 6, ..., 98
      | boost::adaptors::transformed([](int n) { return n*2; }), // 要素を全て2倍にする; 0, 4, 8, 12, ..., 196
    std::ostream_iterator<int>(std::cout, " "));
  // output: 0, 4, 8, 12, ..., 196


  // 文字列を作ってみる
  std::string str;
  boost::copy(
    boost::irange<char>('a', 'z'+1) // 'a'~'z'の要素のrangeを作成; abcd...z
      | boost::adaptors::transformed([](char c) { return c - ('a' - 'A'); }) // 要素を全て大文字にする; ABCD...Z
      | boost::adaptors::reversed, // 要素を逆順にするZYXW...A
    std::back_inserter(str));
  std::cout << str << std::endl; // output: ZYXW...A
}

ちなみに、adaptorは必ずパイプに使うものと関数として使うものとがあり、boost::adaptors::transformに対するboost::adaptors::transformedboost::adaptors::strideに対するboost::adaptors::stridedのように対応関係にあります。

##adaptorを自作してみよう
黒魔術に見えますが実はそんなに難しくないです。adaptorがどうやって出来ているのかを知ることでいざコンパイルエラーが出てしまった時にも対処がしやすいです。ということで、簡単なadaptorを自作してみましょう。ここではfor_eachedというものを作ってみます。

必ず対になるという慣習を守ることにします。と言っても、for_eachならもう片っぽはあるんですけどね。

boost::for_each(range, [](value_type e) { std::cout << e; }); // 普通のfor_each
range | for_eached([](value_type e) { std::cout << e; }); // pipe用のfor_each

std::string str;
boost::copy(
  boost::irange<char>('a', 'z'+1)
    | for_eached([](char e) { std::cout << e; }) // abcd...zと標準出力に出力する
    | boost::adaptors::transformed([](char c) { return c - ('a' - 'A'); })
    | for_eached([](char e) { std::cout << e; }) // ABCD...Zと標準出力に出力する
    | boost::adaptors::reversed,
  std::back_inserter(str));

なんか微妙ですが、まあこんなものを作りたいと思うわけです。基本的には、タグディスパッチ(tag dispatching)という手法を使います。関数名はoperator|で固定で、第一引数はRange&またはconst Range&でなくてはなりませんね。戻り値は第一引数と同じものを返します。なにやら出力ストリーム演算子とそっくりですね。全くこの手の悪習には困ります。
見ての通りfor_eachedはテンプレートを使わなきゃいけないので、テンプレート関数にしましょう。テンプレートが不要であればクラスを使って、コンストラクタで引数を受け取って直接タグディスパッチをかましてもいいですね。
とにかくここではテンプレート関数でfor_eachedを作成して、operator|の第二引数に相当する型を返してやればいいわけです。
それでは、operator|の第二引数に相当する型から作りましょう。for_eached_tagという名前にしました。部分特殊化によってoperator|のオーバーロードを正確に行うためだけのクラスなので、受け取った関数をラップするだけになっています。
そうしたら、operator|を作ります。ここでrangeと関数が揃うので、for_eachを実行してあげます。
最後にoperator|を呼び出すためのfor_eached_tagを作るための関数、for_eachedを作ってあげれば完成です。

ね、簡単でしょう。

template<class UnaryFunction>
class for_eached_tag
{
  UnaryFunction f;

public:
  for_eached_tag(UnaryFunction f) noexcept : f(f) {}
  template<class T>
  auto operator()(T&& arg)
    ->decltype(f(std::forward<T>(arg)))
    { return f(std::forward<T>(arg)); }

  template<class T>
  auto operator()(T&& arg) const
    ->decltype(f(std::forward<T>(arg)))
    { return f(std::forward<T>(arg)); }
};

template<class Range, class UnaryFunction>
static inline Range& operator|(Range& r, for_eached_tag<UnaryFunction> func)
  { boost::for_each(r, func); return (r); }
template<class Range, class UnaryFunction>
static inline const Range& operator|(const Range& r, for_eached_tag<UnaryFunction> func)
  { boost::for_each(r, func); return (r); }

template<class UnaryFunction>
static inline for_eached_tag<UnaryFunction> for_eached(UnaryFunction func)
  { return for_eached_tag<UnaryFunction>(func); }

すでにqiitaに素晴らしい紹介があったので不要かとも思いましたが、コンテナへ変換してくれるアダプタも作ってみましょう。目指す形はこんな感じです。やりましたね、もうboost::copyを挟む必要はありません。

std::vector<int> vec = containerize(boost::irange(0, 100)); // 関数版
std::string str = boost::irange<char>('a', 'z'+1) | containerized; // pipe版

ここでは戻り値による関数のオーバーロードに似たイディオムを使います。
先ほどとは少し手順が変わっているのが面白いですね。最初に実際に変換を担当するクラスを作成し、それを使ってcontainerize関数を作成します。そうしたらタグを作り、今回は引数は要らないのでタグをそのままoperator|に渡してやればOKです。

template<class Range>
class containerizer
{
  const Range& range;

public:
  containerizer(const Range& r) noexcept : range(r) {}

  template<class To>
  operator To() const // これのおかげで戻り値によるオーバーロードもどきができる
  {
    using std::begin; using std::end;
    return To(begin(range), end(range));
  }
};

template<class Range>
static inline containerizer<Range> containerize(Range&& range)
  { return containerizer<Range>(std::forward<Range>(range)); }

struct containerized_tag {};
static constexpr containerized_tag containerized; // 今回はただのタグ

template<class Range>
static inline containerizer<Range> operator|(
    Range&& range, containerized_tag)
  { return containerize(std::forward<Range>(range)); } // operator|でcontainerizeを実行する

#おわりに
結構大変な記事になってしまいましたが、これでもBoost.Rangeのごく一部しか紹介できていません。上のものをさらに楽に書くためのヘルパ関数などもあるので、気に入っていただけたらぜひBoostをダウンロードしてみて、使ってみてください。このライブラリのように、Boostにはヘッダオンリーで使えるライブラリがたくさんあります。この記事がBoostを使うきっかけになれば幸いです。
予定していたより長くなってしまいましたが、ここまで読んでいただけた方がいらっしゃったらありがとうございます。
また間違い、typo、私の理解の不足や説明の不足があればぜひ指摘をお願いいたします。

誤字が大量にありますね。
直したつもりですがまだまだありそう。

28
21
1

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
28
21

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?