Range を使おう

これは C++11 Advent Calendar 2011 の参加記事 (15日目) です.
この記事では「Iterator ではなく Range を使おう」という話をします.既に何度も言われている話ではありますが,私も一度書いてみたかったのでこの機会に書きます.でもそんなに新しいことは言わないと思います.
話の流れとしては,従来の Iterator にはこんな欠点が → それ Range ならうまく書けるよ → さらに Range 使ってこんなこともできるよ → C++11 での Range という感じになります.

従来の Iterator の問題点

IteratorSTL の主要なコンセプトで,Container と Algorithm を橋渡しする役割を持っています.

std::vector<int> v = { 8, 4, 3, 7, 6, 5, 2, 1 };
std::sort(v.begin(), v.end());

この例で,std::vector<int> と std::sort は互いを意識しておらず,Iterator を介してのみつながっています.Iterator は Container と Algorithm の結合度を下げ,それらが独立に実装されることを可能にしているわけです.
このように Iterator は素晴らしいものですが,問題点もあります.ここでは Iterator の問題点を2つ挙げてみます.
1つは,分かりにくいという点です.上の例について言えば,ソートは1つの範囲に対してなされるものであって,"Iterator" 2つを引数に取るインターフェースは非直観的だということです.また,呼び出すのも面倒です.
もう1つは,危険だという点です.上の例にちょっと手を加えてみましょう.

std::vector<int> v = { 8, 4, 3, 7, 6, 5, 2, 1 }, w;
std::sort(v.begin(), w.end());

作為的すぎる気がしますが,もう少し複雑なコードの中ではありうるミスです.コンパイルは警告もなく通り,実行すると良くてクラッシュ,悪くて広範囲のデータを破壊して訳の分からないことになります.
そして,これに std::sort の側で対処することはできません.なぜなら,2つの Iterator が同じ範囲の要素を指していること,言い換えれば2つの Iterator を比較してよいことを知る一般的な方法はないからです *1
これらの問題点をうまく解決してくれるのが Range です.

Range を使う

Range は従来の Container を一般化したコンセプトで,文字通り1つの範囲を表します.全ての Container および配列が Range であるほか,Boost.Range などのライブラリで多くの Range が提供されています.基本的に,Range は Range Algorithm と呼ばれるアルゴリズムに渡して使います.
では上の例を Range で書き直して,問題点が解消されていることを確認しましょう.std::vector<int> は既に Range なので,std::sort を Range Algorithm である boost::sort で置き換えます.

std::vector<int> v = { 8, 4, 3, 7, 6, 5, 2, 1 };
boost::sort(v);

とても直観的なコードになりました.また,誤って複数の範囲を渡してしまうこともありません.Range が Iterator より良い選択であることが分かると思います.

もっと Range を使う

Boost.Range には STL のものと同名の Range Algorithm が多数あり,上の boost::sort もその1つです.基本的には,Iterator 2つを取るところが Range 1つを取るように変わっていると考えて構いません.

void disp(int i) { std::cout << i << '\n'; }

std::vector<int> const v = { 1, 2, 3, 4, 5 };
// もう古い!
// std::for_each(v.begin(), v.end(), &disp);
boost::for_each(v, &disp);

また先述の通り,配列も Range として使えます.

int const a[] = { 1, 2, 3, 4, 5 };
boost::for_each(a, &disp);

これだけでも十分 Range の便利さが分かると思いますが,Range の威力は,それを最大限に活用したライブラリがあることでより一層増します.そのようなライブラリとして,ここでは PStade.Oven を紹介しましょう.次の例は,Range 中のユニークな要素の数を求めるものです.

using namespace pstade::oven;

void oven_example()
{
    int const a[] = { 0, 1, 6, 2, 6, 6, 5, 2, 7, 5 };
    BOOST_CHECK(distance(a | sorted | uniqued) == 6);
}

コードは簡潔で,sorted や uniqued という言葉から,容易にその意味を察することができます.この威力を感じて下さい.

C++11 での Range

C++11 で Range Algorithm が標準ライブラリに取り入れられることはありませんでしたが,言語のコア機能の一部で Range をサポートしています.Range-based for です.これは他の言語で言うところの foreach に当たります.使ってみましょう.

// 基本
int a[] = { 1, 2, 3, 4, 5 };
for (auto i : a)
    std::cout << i << '\n';

// 要素を変更する
for (auto &i : a)
    i *= 2;

// break,continue も可
std::vector<std::string> const v = { "foo", "bar", "baz", "qux", "quux" };
for (auto const &s : v) {
    if (s[0] == 'q')
        break;
    std::cout << s << '\n';
}

// { e1, ..., eN } の形を受け取ることもできる
for (auto i : { 1, 2, 3, 4, 5 })
    std::cout << i << '\n';

boost::find_if + ラムダ式でも近いことはできますが,C++11 のラムダ式は少々貧弱ですし,直観的に書ける range-based for の出番は多いと思います.GCC 4.6 以降,Clang 3.0 以降でサポートされているので,積極的に使っていきましょう.え,VC?*2

おわりに

ちょっと gdgd になりましたが,Range を使おうという話でした.
次は id:spinor さんによるコンセプト入門です.

*1:この例に関してのみ言えば,std::vector<int>::iteratorvector の情報を埋め込むことで適切な assertion が可能かもしれませんが,汎用性は全くありません

*2:代わりに独自拡張の for each が使えるかもしれません