📘

C++のrangeを理解しつつアダプタを自作してメタプログラミングを学ぶ(1)

2022/06/06に公開

欲張りセットなタイトルですが、やっていきましょう。

なぜrangeが必要なのか

ある程度プログラミングしていると、配列やそれに類するもの(C++ならコンテナ、C#ならコレクションと呼ばれる型)を関数の引数に渡したり、あるいは返り値として返したり、といったことがやりたくなることがあります。しかし、そういったオブジェクトの型をそのまま引数や返り値に利用するのは、色々な意味で巧くないコードになりがちです。

例えば、文字列の配列を受け取る関数を作りたいとします。C++でのスタンダードなコンテナといえば std::vector ですから std::vector<std::string> を引数に取ればいいや、と思うかもしれません。しかし std::list<std::string> でデータを保持している処理でその関数を利用したい、ということもあり得ます。そうすると std::vector<std::string> にデータを詰め直す作業が必要になり、無駄なコストが生じます。

また std::vector はアロケータを指定して置き換えることができますが、そうするとデフォルトのアロケータを利用している std::vector<TValue> とは異なる型(std::vector<TValue, TAllocator>)になります。つまり、std::vector<std::string> を型として指定すると、アロケータを差し替えたものは受け付けません!ということになるのです。

このように「コンテナの先頭から末尾まで、順番に要素を取り出してアクセスしたいだけなのに、内部実装まで規定してしまう型を指定するのは巧くない」ということを、まずは実感していただきたいです。(もちろん、内部処理で扱うコンテナの種類が確定しているなら、その型を直接利用することは何ら問題ありません)

C#の場合は IEnumerable というインタフェースが全てのコレクションの基底になっているので、これを利用することであらゆるコレクションを受け入れることができます。さすが後発の言語だけあってスマートな仕様です。さらには LINQ を使って色々な処理を噛ませたイテレートが可能です。そこらへんは https://zenn.dev/rita0222/articles/9950ff78cab71371c085 にも書いてあるので、読んでみてください。

で、我らがC++先生にもそんなインタフェースがあればいいんですが、値型でゴリゴリにパフォーマンスを追求することが至上命題であるC++においてインタフェースは甘えです。C++が古くから提示してきたのは「イテレータがあれば十分でしょ!」という回答でした。

std::vector<string> vec;

for (std::vector<string>::iterator it = vec.begin(), e = vec.end(); it != e; ++it) {
  std::cout << *it << std::endl;
}

上記の for ループは、std::vectorstd::liststd::set になったとしても、意図通りに動作します。その統一性は、それはそれでありがたいんですが、コンテナをイテレートするのにわざわざこれをタイプせにゃならんかったり、イテレータなるものを意識したコードを書かなくてはならないのは、利便性に難があると言わざるを得ませんでした。

そこでC++11では範囲for文(range-based for)が導入され、いかなるコンテナにおいても次のようなコードでイテレートできるようになりました。

std::vector<string> vec;

for (auto& str : vec) {
  std::cout << str << std::endl;
}

上記2つのコードはほぼ等価です。後者のコードは前者のように展開されるものと考えてください。逆に言えば、範囲for文で : の後ろに書ける型は、前者のコードで利用されている要件を満たせばよい、ということになります。

  • begin()end() というメンバ関数を持ち、それらが範囲の先頭と末尾を示すイテレータを返すこと
  • 上記で得られるイテレータは * 演算子で要素への参照が取得でき、++ 演算子で次の要素を指し示すこと
  • イテレータが end() と等しいと判定された時点でループを終了すること

これらを満たせば、どんな型でも範囲for文に突っ込めるということになります。標準ライブラリのコンテナはすべてこの要件を満たしていますし、boostなどで提供するコンテナもこれに準じています。もしコンテナとしての役割を持つ型を作るなら、これに沿わない手はないでしょう。

ちなみにC++20以降では、この要件は range concept として定義されています。範囲for文で使えるために最低限の要件に加えて、更に細かな条件(begin()を呼び出しても何もオブジェクトに影響を与えない、など)が定められたり、逆に制限が緩和されたり(begin()とend()が同じ方じゃなくてもよい、など)しています。詳しくは こちら をご参照ください。

range の意義と定義が理解できたならば、「++で素直に1つずつ次に進めるのではなく、条件を満たすものまで進めるようにする」とか「*で得られる参照を、異なる値に変換できるんじゃないか」などと妄想が膨らみます。膨らみませんか?膨らませましょう。

それです!それがLINQです!

間違えました。それがまさに range アダプタであり、それで得られるものが view です!
というわけで、次回はC++17以前の環境で range アダプタを自作して、メタプログラミングに親しんでもらおうと思います。

Discussion