[C++] <ranges>のviewを見る3 - iota_view

3 min読了の目安(約3000字TECH技術記事

iota_view

iota_viewは渡された2つの値をそれぞれ始点と終点として、単調増加するシーケンスを作成するViewです。
整数型に限定するならば、init, boundの2つの値を渡すと[init, bound)の範囲で1づつ増加していく数列を生成します。

#include <ranges>

int main() {
  std::ranges::iota_view iv{1, 10};

  for (int n : iv) {
    std::cout << n; // 123456789
  }
}

また、引数1つだけで構築した場合は終端のない無限列を生成します。

#include <ranges>

int main() {
  std::ranges::iota_view iv{1};

  for (int n : iv) {
    std::cout << n;     // 1234567891011121314151617181920
    if (n == 20) break; // 何かしら終わらせる条件がないと無限ループ
  }
}

このような無限列は、range adaptorと呼ばれるViewと組み合わせる事で有効活用することができます(後の方で紹介予定)。

基本的には整数列を生成するために使用すれば良いのですが、この実体はインクリメント可能であり距離を定義できさえすればどんな型の単調増加シーケンスでも作成可能です。これはstd::weakly_incrementableコンセプトによって制約されます。
例えば、ポインタ型やイテレータ型のシーケンスを作成可能です。

int main() {
  
  int array[] = {2, 4, 6, 8, 10};
  
  // ポインタのインクリメントシーケンス
  std::ranges::iota_view iva{std::ranges::begin(array), std::ranges::end(array)};

  for (int* p : iva) {
    std::cout << *p;  // 246810
  }
  
  std::cout << '\n';
  
  std::list list = {1, 3, 5, 7, 9}; 
  
  // listイテレータのインクリメントシーケンス
  std::ranges::iota_view ivl{std::ranges::begin(list), std::ranges::end(list)};

  for (auto it : ivl) {
    std::cout << *it; // 13579
  }
}

正直このジェネリックな振る舞いは使いどころがあるのか分かりません。頭の片隅に置いておくと役立つこともあるでしょうか・・・?
なお、<ranges>Viewはおおよそ全てがこの様になるべく無駄にジェネリックに動作可能なように設計されています。

なお、浮動小数点数型はstd::weakly_incrementableを満たさないのでiota_viewでは使用できません。距離を整数値で定義できないのが原因のようです。

遅延評価

iota_viewによって生成されるシーケンスはiota_viewオブジェクトを構築した時点では生成されておらず、その生成は遅延評価されます。

具体的には、iota_viewオブジェクトから得られるイテレータのインクリメント(++i/i++)のタイミングで1つづつシーケンスの要素が計算されます。

int main() {
  std::ranges::iota_view iv{1, 10}; // この段階ではまだ何もしてない

  auto it = std::ranges::begin(iv); // この段階ではまだ何もしてない

  int n1 = *it; // 初項(1)が得られる
  ++it;         // ここで次の項(2)が計算される
  it++;         // ここで次の項(3)が計算される
  int n2 = *it; // 3番目の項(3)が得られる  
}

rangeカテゴリ

iota_viewの表すrangeはその要素型によってrangeカテゴリが変化します。整数型で使用する分には常にrandom access rangeですが、そのほかの型の場合は可能な操作によって弱くなる可能性があります。例えば、std::listのイテレータによるiota_viewオブジェクトはbidirectional rangeになります。

random access rangeとなるiota_viewのシーケンスは、そのイテレータに対して--, +=, -=等でシーケンス上をほぼ自由に移動でき、bidirectional rangeとなる場合は++, --によって前後方向への移動が可能になります。

range factories

iota_viewにもrange factoryとなる関数オブジェクトが用意されています。

#include <ranges>

int main() {
  for (int n : std::views::iota(1, 10)) {
    std::cout << n;   // 123456789
  }
  
  std::cout << '\n';

  for (int n : std::views::iota(1)) {
    std::cout << n;   // 1234567891011121314151617181920
    if (n == 20) break;
  }
}

このstd::views::iotaはカスタマイゼーションポイントオブジェクトで、1つか2つの引数を受け取りその引数をそのまま転送してiota_viewオブジェクトを生成し返します。