C++ の Ranges と View

最終更新: 2021-10-01 09:45:43 +0800 +0800
原文は英語で書かれています。あなたはAI翻訳版を読んでいます。

ranges ライブラリは、反復する能力を所有権、走査の強さ、遅延変換から分離します。range は反復境界を提供し、view はアダプターパイプラインに適した range であり、アダプターは反復が要求したときに要素を評価する新しい view を構築します。

cpp
#include <ranges>
#include <vector>

std::vector<int> source{1, 2, 3, 4, 5};
auto even_squares = source
    | std::views::filter([](int value) { return value % 2 == 0; })
    | std::views::transform([](int value) { return value * value; });

even_squares を構築してもすべての要素は変換されません。反復によって述語と変換が呼び出されるため、反復前の source への変更は、ソースコンテナーの無効化規則に従って観測される可能性があります。

コア概念 #

型の左辺値に対して std::ranges::begin と std::ranges::end が有効な場合、その型は std::ranges::range をモデル化します。イテレーター型とセンチネル型は同じである必要はありません。この概念は反復が可能であることだけを確立し、所有権、サイズ、複数回走査、連続性、イテレーターのライフタイムについては何も述べません。

cpp
template<class T>
concept range = requires(T& value) {
    std::ranges::begin(value);
    std::ranges::end(value);
};

標準の定義も同等の意図を持ちますが、独自の exposition-only の詳細を使用します。std::vector<int> と std::vector<int>& はどちらも range をモデル化します。

std::ranges::view は、軽量で適応可能な range オブジェクトとして range を精密化します。その構文上の定義は range、std::movable、std::ranges::enable_view を組み合わせますが、重要なのは意味上の要件です。view は定数時間の移動構築を持つ必要があり、コピー、移動、代入、破棄の要件によって、アダプターパイプラインが予期せず要素単位の作業を行わないことが保証されます。std::vector は range ですが、要素のコピーが定数時間ではないため view ではありません。std::string_view、std::ranges::ref_view、多くの生成された range は view です。

プログラム定義の view は、一般に std::ranges::view_interface または std::ranges::view_base から派生します。view_interface<D> は CRTP ヘルパーで、D が必要な range 機能を持つ場合にのみ、empty、front、back、data、size、インデックス指定などのメンバーを条件付きで提供します。プログラムは、view 要件を実際に満たす cv 非修飾のプログラム定義型に限って enable_view を true に特殊化できます。特殊化は意味上の約束であり、制約の回避ではありません。

viewable_range と views::all #

viewable_range は別の問いに答えます。特定の range 式を view に変換できるかどうかです。構築可能な view と、適切な左辺値または移動可能な非 view 入力を受け入れ、問題のある initializer-list ケースを除外します。仕様は C++20 以降で進化しているため、アプリケーションコードでは定義を再現せず概念を使用すべきです。

std::views::all は変換境界です。概念的には、構築可能な入力 view を保持し、非 view の左辺値を ref_view でラップし、適格な非 view の右辺値を owning_view でラップします。

cpp
std::vector<int> values{1, 2, 3};
auto borrowed = std::views::all(values);              // ref_view
static_assert(std::ranges::view<decltype(borrowed)>);

auto owned = std::views::all(std::vector<int>{1, 2}); // owning_view

ref_view はソースオブジェクトのライフタイムを延長しません。そのため、ローカルコンテナーを参照する view を返すのは誤りです。すべての view が非所有とは限りません。owning_view は基底 range を所有し、single_view は値を格納します。ライフタイムは「view」という語からではなく、具体的な view とソース式から判断してください。

borrowed_range は別の独立したプロパティです。具体的な range のイテレーターが range オブジェクトの破棄後も存続できるため、アルゴリズムが右辺値 range から std::ranges::dangling ではなくイテレーターを返すことを許可します。すべてのイテレーターが永久に有効という意味ではありません。コンテナーの変更と基底ストレージのライフタイムは引き続き重要です。

ファクトリーとアダプター #

ファクトリーはソース view を作成し、アダプターは既存の viewable_range を変換します。

機能種類標準の基準
views::empty<T>空のソース viewC++20
views::single(value)1 つの格納値C++20
views::iota(first, bound)増加する生成シーケンスC++20
views::istream<T>(stream)遅延抽出されるストリーム値C++20
views::repeat(value, bound)繰り返される値C++23
views::cartesian_product(...)直積C++23
views::counted(iterator, count)カウントされたソース rangeC++20

views::transform、filter、take、drop、reverse、split、lazy_split、join、common、elements、keys、values は C++20 のアダプターです。C++23 では adjacent、slide、chunk、stride、zip、zip_transform、join_with、as_const などのアダプターが追加されます。利用可能性は、選択した言語モードと標準ライブラリの実装の両方に依存します。

アダプターオブジェクトは直接呼び出すことも部分適用することもできます。

cpp
auto doubled = std::views::transform(source, [](int value) {
    return value * 2;
});

auto double_each = std::views::transform([](int value) {
    return value * 2;
});
auto also_doubled = double_each(source);

部分適用された結果は range アダプタークロージャオブジェクトです。クロージャ closure と viewable_range range について、range | closure は closure(range) と同じ効果を持ちます。クロージャは合成できます。

cpp
auto useful = std::views::filter(is_valid) | std::views::transform(normalize);
auto result = source | useful;

C++23 は移植可能なユーザー定義クロージャ向けに std::ranges::range_adaptor_closure を提供します。C++23 より前は、実装内部のクロージャ基底はアプリケーションインターフェイスではありませんでした。

Range のプロパティとユーティリティ #

精密化概念は、range の独立したプロパティを記述します。

概念意味
input_range1 回だけの入力走査をサポートする。
forward_range複数回の前方走査をサポートする。
bidirectional_range後方走査をサポートする。
random_access_range定数時間のインデックス付きイテレーター移動をサポートする。
contiguous_range要素が連続したストレージに配置されている。
sized_rangeranges::size が償却定数時間で有効である。
common_rangeイテレーターとセンチネルが同じ型である。
borrowed_rangerange の破棄だけを理由として右辺値由来のイテレーターがダングリングする必要がない。

走査概念は input から contiguous へ互いに精密化します。sized_range、common_range、borrowed_range は別々の次元を表します。C++23 は constant_range を追加します。プロジェクトの基準が提供する場合にのみ使用してください。

カスタマイズポイントオブジェクトは、ジェネリックコードで配列、メンバー、ADL を正しく扱います。

cpp
auto first = std::ranges::begin(source);
auto last = std::ranges::end(source);
auto count = std::ranges::size(source);

その他のユーティリティオブジェクトには cbegin、cend、rbegin、rend、empty、data、cdata、ssize があります。.begin() メンバーがあると仮定するより、これらを優先してください。

Range 型エイリアスはイテレーターとプロキシ参照の動作を保持します。

cpp
template<std::ranges::range R>
using iterator_t = std::ranges::iterator_t<R>;

template<std::ranges::range R>
using value_t = std::ranges::range_value_t<R>;

template<std::ranges::range R>
using reference_t = std::ranges::range_reference_t<R>;

関連するエイリアスには sentinel_t、range_difference_t、range_size_t、range_rvalue_reference_t があります。const 指向のエイリアスである const_iterator_t と range_const_reference_t には C++23 の利用可能性制約があります。

アルゴリズム、射影、具体化 #

Range アルゴリズムは range を直接受け取り、主操作の前に射影を適用できます。射影は 1 つのアルゴリズムが要素を観測する方法を変更し、transform view は反復が生成するものを変更します。

cpp
struct Record {
    int id;
    std::string name;
};

std::ranges::sort(records, {}, &Record::name);

空の比較関数はデフォルトの順序を選択し、射影はメンバー名を比較関数へ提供します。射影は変換された値を具体化しません。

所有される結果、ソース変更後の安定した走査、または eager な境界が必要な場合は view を具体化します。C++23 では std::ranges::to が変換を直接表現します。

cpp
auto output = std::ranges::to<std::vector<int>>(even_squares);

C++20 を基準にする場合は、コンテナーがその組をサポートする場合にのみ、view のイテレーターとセンチネルから互換性のあるコンテナーを構築します。または、適切な range/イテレーターインターフェイスを通じて挿入します。具体化は遅延処理を実行し、別の結果を確立する時点です。コンテナー API の選択による偶然の副作用ではなく、意図的に行うべきです。

コメント

GitHub で閲覧・コメントする(投稿にはログインが必要です)