[C++] <ranges>のviewを見る8 - take_view
take_view
take_view
は元となるシーケンスの先頭から指定された数だけ要素を取り出したシーケンスを生成するViewです。
#include <ranges>
int main() {
// 先頭から5つの要素だけを取り出す
std::ranges::take_view tv{std::views::iota(1), 5};
for (int n : tv) {
std::cout << n; // 12345
}
}
iota_view
の生成する無限列のように無限に続くシーケンスから決められた数だけを取り出したり、rangeアルゴリズムにおいて他の操作を適用したシーケンスから(先頭に集められた)最終的な結果を取り出す時などに活用できるでしょう。
オーバーランの防止
意地悪な人はきっと、take_view
に元となるシーケンスの長さよりも長い数を指定したらどうなるん?と思うことでしょう。残念ながら?これは対策されています。
take_view
のイテレータは元となるシーケンス(r
とします)の種別によって3つのパターンに分岐します。
-
r
がsized_range
であるならば、次のいずれか-
r
がrandom_access_range
なら、r
の先頭イテレータをそのまま利用 - それ以外の場合、
std::counted_iterator
にr
の先頭イテレータと与えられた長さとr
の長さの短い方を渡して構築
-
- それ以外の場合、
std::counted_iterator
にr
の先頭イテレータと与えられた長さを渡して構築
sized_range
というのはコンセプトで、距離を定義可能なrangeを表します。
std::counted_iterator
はC++20から追加されたiteretor adaptorで、与えられたイテレータをラップして指定された長さだけイテレート可能なものに変換します。
これによって、距離が事前に求まる場合はその距離を超えてイテレートされることはありません。そして、sized_range
ではないrangeに対してもtake_view
の提供するsentinelによって確実にオーバーランしないようにチェックされています。
#include <ranges>
int main() {
using namespace std::string_view_literals;
// 元の文字列の長さを超えた長さを指定する(上記1.1のケース)
std::ranges::take_view tv1{"str"sv, 10};
int count = 0;
// 安全、3回しかループしない
for ([[maybe_unused]] char c : tv1) {
++count;
}
std::cout << "loop : " << count << '\n'; // loop : 3
std::list li = {1, 2, 3, 4, 5};
// 元のリストの長さを超えた長さを指定する(上記1.2のケース)
std::ranges::take_view tv2{li, 10};
count = 0;
// 安全、5回しかループしない
for ([[maybe_unused]] int n : tv2) {
++count;
}
std::cout << "loop : " << count << '\n'; // loop : 5
std::forward_list fl = {1, 2, 3, 4, 5};
// 元のリストの長さを超えた長さを指定する(上記2のケース)
std::ranges::take_view tv3{fl, 10};
count = 0;
// 安全、5回しかループしない
for ([[maybe_unused]] int n : tv3) {
++count;
}
std::cout << "loop : " << count << '\n'; // loop : 5
}
std::counted_iterator
は与えられたイテレータの特性を完全に継承するので、take_view
のrange categoryもまた与えられたrangeと同じになります。
なお、take_view
に負の値を渡すこともできますが、random access rangeの場合以外は未定義動作になります。何かに使えそうな気がしないでもないですが、基本的には避けた方が良いでしょう。
遅延評価
take_view
もまた、遅延評価によってシーケンスを生成します。ただ、take_view
は元となるrangeの極薄いラッパーなので、ほとんどの操作はベースにあるイテレータの操作をそのまま呼び出すだけで、特別な事は行ないません。
take_view
が行なっている事はほぼその長さの管理だけです。それは主に==
による終端チェック時に行われます。また、std::counted_iterator
が使用される場合はそのためにインクリメントのタイミングで残りの距離の計算(単純なカウンタのデクリメントによる)が行われます。
// take_vieww構築時には何もしない
std::ranges::take_view tv{std::views::iota(1), 5};
// イテレータ取得時には元のシーケンスによって最適なイテレータを返す
auto it = std::ranges::begin(tv);
// インクリメントはベースのイテレータをインクリメントする
// counted_iteratorが使用される場合、ここで残りの距離が計算される
++it;
// 間接参照時はベースのイテレータを間接参照するだけ
int n1 = *it; // n1 == 2
// 番兵取得時には元のシーケンスによって最適な番兵を返す
auto fin = std::ranges::end(tv);
// 終端チェック時に与えられた長さと元のシーケンスの長さ、現在の位置に基づいてチェックが行われる
it == fin;
views::take
take_view
に対応するrange adaptor objectがstd::views::take
です。
#include <ranges>
int main() {
for (int n : std::views::take(std::views::iota(1), 5)) {
std::cout << n;
}
std::cout << '\n';
// パイプラインスタイル
for (int n : std::views::iota(1) | std::views::take(5)) {
std::cout << n;
}
}
views::take
はカスタマイゼーションポイントオブジェクトであり、2つの引数を受け取りそれらに応じたViewを返します。その条件は複雑なので割愛しますが、例えばrandom_access_range
かつsized_range
である標準ライブラリのもの(std::span, std::string_view
など)に対しては、与えられた長さと元の長さのより短い方の長さによって構築し直したその型のオブジェクトを返します。
厳密にはtake_view
だけを返すわけではありませんが、結果の型を区別しなければ実質的にtake_view
と同等のViewが得られます。
Discussion