🌀

C++23 <ranges>のviewを見る1 - Repeat view

2023/03/29に公開

views::repeat

views::repeatは与えられた1つの値の繰り返しからなるシーケンスを生成するviewです。これはシーケンスを生成するタイプのviewであるRangeファクトリの1つなので、Rangeアダプタに対する入力として使用するものです。

import std;

int main() {
  for (int n : std::views::repeat(23) | std::views::take(4)) {
    std::ptrint("{}, ", n);
  }
  // 23, 23, 23, 23, 
}

デフォルトでは、与えられた値を永遠に繰り返す無限列が生成されます。

views::repeatには、生成するシーケンスの要素となる値に加えて、2つ目の引数でシーケンスの要素数(繰り返し回数)を指定することもできます。

import std;

int main() {
  for (int n : std::views::repeat(23, 4)) {
    std::ptrint("{}, ", n);
  }
  // 23, 23, 23, 23, 
}

range factoriesとしてのviews::repeat

views::repeatはカスタマイゼーションポイントオブジェクトであり、1つか2つの入力を受けて、それをそのまま転送してrepaet_viewオブジェクトを構築して返します。

repeat_view

repeat_viewviews::repeatの実装詳細であり、views::repeatの実装の全てです。

namespace std::ranges {

  // repeat_viewの宣言例
  template<move_constructible T, semiregular Bound = unreachable_sentinel_t>
    requires (is_object_v<T> && same_as<T, remove_cv_t<T>> &&
              (integer-like-with-usable-difference-type<Bound> ||
               same_as<Bound, unreachable_sentinel_t>))
  class repeat_view : public view_interface<repeat_view<T, Bound>> {
    ...
  };
}

要素型であるTはオブジェクト型でありCV修飾がないことの他にはmove_constructibleであることだけが求められています。

Boundは要素数を抑える型で、デフォルト時はunreachable_sentinelが使用され、要素数を指定する場合はほぼ整数型を求めています。

要素数を指定しない場合(Bound = unreachable_sentinel_tの場合)には.end()から得られる番兵としてunreachable_sentinelが使用され、unreachable_sentinelは任意のイテレータとの比較に常にtrueを返す番兵であるため、イテレータの終端判定を最適化によって除去しやすくなっています。ただし、これによって.begin().end()の型が異なってしまうため、common_rangeではなくなります(要素数を指定する場合は同じイテレータ型が使用されます)。

要素数が指定されている(Boundunreachable_sentinel_tではない)場合にのみ、repeat_viewsized_rangeかつcommon_rangeになります。

動作詳細

repeat_viewの生成するシーケンスは、当然のことながら、その構築時にシーケンス中の要素の全てがどこかの領域に敷き詰められて保持されているものではなく、実際には同じ1つの値を異なるイテレータが参照することによって構成されています。

そのため、そのイテレータの間接参照で得られるものはrepeat_viewに渡されて内部で保持されている値のconst左辺値参照となります。

このことにより、repeat_viewはその要素としてムーブオンリーな型の値からなるシーケンスを生成することができます。

import std;

int main() {
  std::ranges::repeat_view rv{std::make_unique<int>(23), 4};  // ok

  for (const auto& up : rv) {
    std::print("{}, ", &up);
  }
  // 全て同じアドレスが出力される
}

(GCCの実装バグにより動作例なし・・・)

また、この性質によってrepeat_viewは要素型などに関係なく常にrandom_access_rangeとなります。contiguous_rangeではないのは、イテレータの進行と要素のメモリ配置が同期しない(動かない)ためです。

要素数を指定しない場合でもrepeat_viewrandom_access_rangeであるため、その場合のrepeat_viewは進行/後退の両方向について無限列となります。

views::takeviews::dropの特殊対応

repeat_viewは内部に格納している値をそのシーケンスの全ての要素としていることとrepeat_viewそのものに要素数を指定可能であることから、指定された個数の要素を切り出す/スキップするような処理はかなり単純化できる可能性があり、実際にviews::takeviews::dropではそれが行われています。

views::takeの場合は、views::take(r, n)のように呼ばれた時に、r(の素の型R)がrepeat_viewであれば、次のようにrepeat_viewを調整して返します

  • Rsized_rangeである(要素数が指定されている)場合
    • rの内包する値をvalueranges::distance(r)nの短い方をlとして
    • views::repeat(value, l)を返す
  • それ以外の(repeat_viewの要素数が指定されていない)場合
    • rの内包する値をvalueとして
    • views::repeat(value, n)を返す

views::dropの場合は、views::drop(r, n)のように呼ばれた時に、r(の素の型R)がrepeat_viewであれば、次のようにrepeat_viewを調整して返します

  • Rsized_rangeである(要素数が指定されている)場合
    • rの内包する値をvalueranges::distance(r)mmnの短い方をlとして
    • views::repeat(value, m - l)を返す
  • それ以外の(repeat_viewの要素数が指定されていない)場合
    • auto(r)を(rをdecay-copyして)返す

どの場合でも、元のrepeat_viewオブジェクトrから取り出された値(value)はrの値カテゴリによって適切にムーブorコピーされます。

int main() {
  auto inf_rep = std::views::repeat(23);
  
  // 無限`repeat_view`に対するtake/drop
  auto t1 = inf_rep | std::views::take(10);
  // t1 = std::views::repeat(23, 10) と等価
  auto d1 = inf_rep | std::views::drop(10);
  // d1 = auto(inf_rep) と等価
  
  auto fin_rep = std::views::repeat(23, 10);
  
  // 有限`repeat_view`に対するtake/drop、元の長さに収まる場合
  auto t2 = fin_rep | std::views::take(5);
  // t2 = std::views::repeat(23, 5) と等価
  auto d2 = fin_rep | std::views::drop(5);
  // d2 = std::views::repeat(23, 10 - 5) と等価
  
  // 有限`repeat_view`に対するtake/drop、元の長さを超える場合
  auto t3 = fin_rep | std::views::take(15);
  // t3 = std::views::repeat(23, 10) と等価
  auto d2 = fin_rep | std::views::drop(15);
  // d3 = std::views::repeat(23, 10 - 10) と等価
}

views::repeatと各種特性

最後に、views::repeatrepeat_view<T, B>)のrangeとしての各種特性をまとめておきます

  • reference : const T&
  • rangeカテゴリ : random_access_range
  • common_range : Bunreachable_sentinel_tではない時
    • すなわち、要素数を指定した時
  • sized_range : common_rangeと同じ
  • const-iterable : 〇
  • borrowed_range : ×

これらはそれぞれ次のような意味です

  • reference : イテレータの関節参照の結果型
  • rangeカテゴリ : モデルとなる最も強いrangeコンセプト
  • common_range : begin()の戻り値型とend()の戻り値型が同じであるか
  • sized_range : ranges::size()によって定数時間で要素数を得られる
  • const-iterable : const修飾されているときでもrangeとなるか
  • borrowed_range : 範囲の寿命とそこから取得したイテレータの寿命が切り離されている

Discussion