標準範囲

もう聞いたことがあるかもしれませんが、Ranges はマージされ、C++20 の一部になります。これは大きなニュースであり、標準ライブラリが 1998 年に初めて標準化されて以来、おそらく最大の変化を示しています。

お久しぶりです。個人的には、少なくとも 2013 年 11 月に次のように意見を述べて以来、これに向けて取り組んできました。 、」入力範囲に関するブログ投稿。それ以来、私は非常に有能な人々の助けを借りて、最新の範囲ライブラリの構築とその仕様の決定に忙しく取り組んできました.

今後のブログ投稿では、どのようにしてここにたどり着いたか、古いものと新しいものがどのように連携するかについての詳細について説明します (私たちは C++ プログラマーであり、詳細が大好きです) が、この投稿は厳密に内容 .

C++20 には何が追加されますか?

すべてのレンジ TS — そしていくつか — C++20 の一部として出荷されます。次の標準の一部として出荷されるすべての主要機能の便利な表を次に示します。

機能
基本概念 std::Copyable<T>
反復子と範囲の概念 std::InputIterator<I>
新しい便利なイテレータ トレイト std::iter_value_t<I>
より安全な範囲アクセス関数 std::ranges::begin(rng)
プロキシ イテレータのサポート std::iter_value_t<I> tmp =
std::ranges::iter_move(i);
連続イテレータのサポート std::ContiguousIterator<I>
制約付きアルゴリズム std::ranges::sort(v.begin(), v.end());
範囲アルゴリズム std::ranges::sort(v);
制約付き関数オブジェクト std::ranges::less
一般化された callable std::ranges::for_each(v, &T::frobnicate);
予測 std::ranges::sort(employees, less{},
&Employee::id);
範囲ユーティリティ struct my_view : std::view_interface<my_view> {
レンジジェネレーター auto indices = std::view::iota(0u, v.size());
範囲アダプター for (auto x : v | std::view::filter(pred)) {

以下、それぞれについて一言。しかし、最初に、古いコーディングの課題を再検討し、その解決策を標準の C++20 の観点から再構築したいと考えました。

ピタゴラスのトリプル、再訪

数年前、私は範囲を使用してピタゴラス トリプルの無限リストを生成する方法に関するブログ投稿を書きました。最初の 2 つの 2 乗の合計が 3 番目の 2 乗に等しい整数の 3 タプルです。

以下は、標準の C++20 で表示される完全なソリューションです。休憩の後、ソリューションを分解します。

// A sample standard C++20 program that prints
// the first N Pythagorean triples.
#include <iostream>
#include <optional>
#include <ranges>   // New header!

using namespace std;

// maybe_view defines a view over zero or one
// objects.
template<Semiregular T>
struct maybe_view : view_interface<maybe_view<T>> {
  maybe_view() = default;
  maybe_view(T t) : data_(std::move(t)) {
  }
  T const *begin() const noexcept {
    return data_ ? &*data_ : nullptr;
  }
  T const *end() const noexcept {
    return data_ ? &*data_ + 1 : nullptr;
  }
private:
  optional<T> data_{};
};

// "for_each" creates a new view by applying a
// transformation to each element in an input
// range, and flattening the resulting range of
// ranges.
// (This uses one syntax for constrained lambdas
// in C++20.)
inline constexpr auto for_each =
  []<Range R,
     Iterator I = iterator_t<R>,
     IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun)
        requires Range<indirect_result_t<Fun, I>> {
      return std::forward<R>(r)
        | view::transform(std::move(fun))
        | view::join;
  };

// "yield_if" takes a bool and a value and
// returns a view of zero or one elements.
inline constexpr auto yield_if =
  []<Semiregular T>(bool b, T x) {
    return b ? maybe_view{std::move(x)}
             : maybe_view<T>{};
  };

int main() {
  // Define an infinite range of all the
  // Pythagorean triples:
  using view::iota;
  auto triples =
    for_each(iota(1), [](int z) {
      return for_each(iota(1, z+1), [=](int x) {
        return for_each(iota(x, z+1), [=](int y) {
          return yield_if(x*x + y*y == z*z,
            make_tuple(x, y, z));
        });
      });
    });

    // Display the first 10 triples
    for(auto triple : triples | view::take(10)) {
      cout << '('
           << get<0>(triple) << ','
           << get<1>(triple) << ','
           << get<2>(triple) << ')' << '\n';
  }
}

上記のプログラムは以下を出力します:

(3,4,5)
(6,8,10)
(5,12,13)
(9,12,15)
(8,15,17)
(12,16,20)
(7,24,25)
(15,20,25)
(10,24,26)
(20,21,29)

このプログラムは、ピタゴラス数の無限リストを (遅延して) 生成し、最初の 10 個を取得して出力します。以下は、それがどのように機能するかについての簡単な要約です。その過程で、C++20 以降で標準となるソリューションの部分を指摘します。

main()

まず、main を見てみましょう 、トリプルの無限リストを作成し、最初の 10 個を出力します。for_each を繰り返し使用します。 無限リストを定義します。このような使い方:

auto x = for_each( some-range, [](auto elem) {
  return some-view;
} );

意味:some-range 内のすべての要素 、ラムダを呼び出します。このようにして生成されたすべてのビューを遅延収集し、それらを新しいビューにフラット化します。ラムダが view::single(elem) を返す場合 、たとえば — これはちょうど 1 つの要素のビューを返します — 上記は no-op です:最初に carve some-range Nに それぞれ 1 要素の部分範囲を分割し、それらすべてを 1 つの範囲に平坦化します。

その知識があれば、for_each の 3 重にネストされた呼び出しを理解できます。 :

for_each(iota(1), [](int z) {
  return for_each(iota(1, z+1), [=](int x) {
    return for_each(iota(x, z+1), [=](int y) {

このコードは、整数 x のすべての組み合わせを生成しています 、 y 、および z ある順序で (x になるように境界を選択します) と y z より大きくなることはありません 、それらはピタゴラスのトリプルになることはできないため)。各レベルで構造を作成します。単一の範囲 (iota(1)) から始めます。 、以下で説明)、各内部範囲が z の値を共有するすべての組み合わせに対応する範囲の範囲を取得します .これらの内側の範囲自体はさらにサブ範囲に分解され、それぞれが x の値を共有するすべての組み合わせを表します .などなど。

最も内側のラムダには x があります 、 y 、および z トリプルを発行するかどうかを決定できます:

return yield_if(x*x + y*y == z*z,
    make_tuple(x, y, z));

yield_if ブール値を取ります (ピタゴラスのトリプルを見つけましたか? ) とトリプル、およびトリプルを含む空の範囲または 1 要素の範囲のいずれかを出力します。次に、その範囲のセットがフラット化され、フラット化され、ピタゴラス トリプルの無限リストに再びフラット化されます。

次に、その無限リストを view::take(10) にパイプします 、無限リストを最初の 10 要素に切り捨てます。次に、これらの要素を通常の範囲ベースの for で反復処理します ループして結果を出力します。ふう!

このプログラムが何をしているのかを大まかに理解したので、個々のコンポーネントを詳しく見ていきましょう。

view::iota

これは非常に単純なビューです。 Incrementable の 1 つまたは 2 つのオブジェクトを取ります。 タイプ。 2 番目の引数をハーフクローズドの上限として使用して、それらから範囲を構築します (つまり、 指定されていない場合、上限を到達不能なセンチネルと見なします (つまり、 範囲は無限です)。ここでは、整数の範囲を構築するためにそれを使用しますが、イテレータを含め、インクリメント可能な型であれば何でも構いません。

iota」という名前 」は std::iota から来ています 数値アルゴリズムであり、それ自体が興味深い命名の歴史を持っています。

for_each

range-v3 ライブラリには view::for_each が付属しています と yield_if 、しかしそれらはまだ提案されていません。しかし view::for_each view::transform の自明な合成です そして view::join する C++20 の一部であるため、次のように実装できます:

inline constexpr auto for_each =
  []<Range R,
     Iterator I = iterator_t<R>,
     IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun)
       requires Range<indirect_result_t<Fun, I>> {
     return std::forward<R>(r)
       | view::transform(std::move(fun))
       | view::join;
  };

これはオブジェクト for_each を宣言します これは、明示的に指定されたテンプレート パラメーターを持つ C++20 の制約付きジェネリック ラムダです。 「Range 」および「IndirectUnaryInvocable」 」は、名前空間 std にある C++20 の標準概念です。 .引数 r を制約します と fun ラムダの範囲(当たり前)と範囲の値で呼び出すことができる関数です。次に、末尾の requires でラムダをさらに制約します 句、関数の戻り値の型が Range でなければならないことを保証します 同じように。 indirect_result_t C++20 でも標準になります。これは、この反復子を逆参照した結果でこの関数を呼び出すと、どの型が返されるのか?という質問に答えます。

ラムダは最初に範囲 r を遅延変換します view::transform にパイプすることで 、移動 fun in. view:: std:: 内の名前空間です すべての新しい遅延範囲アダプターが存在する場所。 fun以降 Range を返します (それが必要でした!)、変換の結果は範囲の範囲です。それを view::join にパイプします 範囲を 1 つの大きな範囲にフラット化します。

実際のコードである 6 ~ 8 行目は、ライブラリを使用するために厳密には必要ではない制約の海に埋もれているようなものです。私はここで教訓的な目的のために少し衒学的なことをしているので、つまずかないようにしてください。

for_each もとても簡単に書くことができました。 制約付きの汎用ラムダで初期化されたオブジェクトにする代わりに、バニラ関数テンプレートとして。私がオブジェクトを選択したのは、C++20 でラムダを使って概念を使用する方法を示したかったからです。関数オブジェクトには他にも優れたプロパティがあります。

yield_if

yield_if 概念的にはより単純ですが、少し手間がかかります。これは、ブール値とオブジェクトを受け取る関数であり、空の範囲 (ブール値が false の場合)、またはオブジェクトを含む長さ 1 の範囲を返します。そのためには、maybe_view という独自のビュー タイプを記述する必要があります。 、C++20 には存在しないためです。 (少なくともまだです。提案があります。)

std::view_interface の助けを借りて、ビューの記述が少し簡単になります begin() からボイラープレートの一部を生成します と end() あなたが提供する機能。 view_interface .size() のようないくつかの便利なメンバーを提供します 、 .operator[].front() 、および .back() .

maybe_view 以下に再掲します。 std::optional に関して、それがどのように自明に実装されているかに注目してください。 と std::view_interface .

template<Semiregular T>
struct maybe_view : view_interface<maybe_view<T>> {
  maybe_view() = default;
  maybe_view(T t) : data_(std::move(t)) {
  }
  T const *begin() const noexcept {
    return data_ ? &*data_ : nullptr;
  }
  T const *end() const noexcept {
    return data_ ? &*data_ + 1 : nullptr;
  }
private:
  optional<T> data_{};
};

maybe_view を取得したら 、 yield_if の実装 も些細なことです。空の maybe_view を返します 、またはブール引数に応じて、単一の要素を含むもの。

inline constexpr auto yield_if =
  []<Semiregular T>(bool b, T x) {
    return b ? maybe_view{std::move(x)}
             : maybe_view<T>{};
  };

以上です。このプログラムは view::iota の使用方法を示しています 、 view::transformview::joinview_interface 、およびいくつかの標準的な概念を実装して非常に便利なライブラリ機能を実装し、それを使用していくつかの興味深いプロパティを持つ無限リストを構築します。 Python または Haskell でリスト内包表記を使用したことがある場合、これは非常に自然に感じるはずです。

しかし、これらの機能は、C++20 でサポートされている範囲のごく一部にすぎません。以下では、投稿の上部にある表の各行を確認し、それぞれの例を示します.

基本概念

C++20 標準ライブラリは、ユーザーがテンプレートを制約し、ユーザーにとって意味のある上位レベルの概念を定義するために独自のコードで使用できる、一般的に有用な概念定義のホストを取得しています。これらはすべて新しい <concepts> に存在します ヘッダー、および Same<A, B> のようなものが含まれます 、 ConvertibleTo<From, To>Constructible<T, Args...> 、および Regular<T> .

たとえば、enqueue を持つスレッド プール クラスがあるとします。 引数なしで呼び出し可能なものを取るメンバー関数。今日では、次のように記述します:

struct ThreadPool {
  template <class Fun>
  void enqueue( Fun fun );
};

このコードを読んでいるユーザーは疑問に思うかもしれません:タイプ Fun の要件は何ですか? ? C++20 の std::Invocable を使用して、コードで要件を強制できます。 コンセプト、および最近追加された短縮関数構文のサポート:

#include <concepts>

struct ThreadPool {
  void enqueue( std::Invocable auto fun );
};

これは fun と述べています 引数なしで呼び出し可能である必要があります。 template <class ...> と入力する必要さえありませんでした ! (std::Invocable<std::error_code &> auto fun std::error_code への参照で呼び出し可能でなければならない関数を宣言します 、別の例を挙げる)

反復子と範囲の概念

標準ライブラリの大部分は、コンテナー、イテレーター、およびアルゴリズムに関連しているため、この分野の概念語彙が特に豊富であることは理にかなっています。 Sentinel<S, I> のような便利な概念定義を探します 、 InputIterator<I> 、および RandomAccessIterator<I> <iterator>IndirectRelation<R, I1, I2> のような便利な構成に加えて、ヘッダー R をテストする イテレータ I1 を逆参照した結果に関係を課します と I2 .

たとえば、コードベースに SmallVector というカスタム コンテナ タイプがあるとします。 std::vector のように 、範囲を示す 2 つの反復子を渡すことで初期化できます。 <iterator> のコンセプトでこれを書くことができます および <concepts> 次のように:

template <std::Semiregular T>
struct SmallVector {
  template <std::InputIterator I>
    requires std::Same<T, std::iter_value_t<I>>
  SmallVector( I i, std::Sentinel<I> auto s ) {
    // ...push back all elements in [i,s)
  }
  // ...

同様に、この型は、新しい <ranges> で定義された概念を使用して範囲を直接受け取るコンストラクターを取得できます。 ヘッダー:

  // ... as before
  template <std::InputRange R>
    requires std::Same<T, std::range_value_t<R>>
  explicit SmallVector( R && r )
    : SmallVector(std::ranges::begin(r),
                  std::ranges::end(r)) {
  }
};

新しい便利なイテレータ トレイト

C++17でイテレータの値の型を知りたい場合 Itypename std::iterator_traits<I>::value_type と入力する必要があります .それは一口です。 C++20 では、std::iter_value_t<I> に大幅に短縮されます。 .以下は、新しい短い型エイリアスとその意味です:

新しいイテレータ型エイリアス 旧版
iter_difference_t<I> typename iterator_traits<I>::difference_type
iter_value_t<I> typename iterator_traits<I>::value_type
iter_reference_t<I> typename iterator_traits<I>::reference
iter_rvalue_reference<I> 同等のものはありません。以下を参照してください

iter_category_t<I> はありません タグのディスパッチは時代遅れになったため、イテレータのタグの型を取得します。イテレータ concept でディスパッチできるようになりました 言語サポートを使用する場合、タグは必要ありません。

セーフ レンジ アクセス機能

std::begin の何が問題なのですか? と std::end ?サプライズ!それらはメモリセーフではありません。このコードが何をするか考えてみてください:

extern std::vector<int> get_data();
auto it = std::begin(get_data());
int i = *it; // BOOM

std::begin const の 2 つのオーバーロードがあります const 以外 左辺値。問題は、右辺値が const にバインドされることです 左辺値参照、ダングリング イテレータ it につながる その上。代わりに std::ranges::begin を呼び出していたら 、コードはコンパイルされませんでした.

ranges::begin 以外にも素晴らしい点があります。 using std::begin; を入力するのを忘れないように、ADL の 2 ステップを実行します。 汎用コードで。つまり、begin() にディスパッチします。 フリー関数は ADL によって検出されますが、ただし、Iterator を返す場合のみです。 .これは、std::begin では得られない追加のサニティ チェックです。 .

基本的には ranges::begin を優先します C++20 以降のすべての新しいコードで。

Prvalue およびプロキシ イテレータのサポート

C++98 の反復子のカテゴリはかなり制限的です。イテレータが operator* から一時 (つまり、prvalue) を返す場合 、モデル化できる最強のイテレータ カテゴリは InputIterator でした . ForwardIterator 必須 operator* 参考までに返します。つまり、値によって単調に増加する整数を返す単純な反復子 (たとえば、ForwardIterator を満たすことができない) を意味していました。 .残念ですが、これは便利なイテレータです。より一般的には、オンデマンドで値を計算するイテレータは ForwardIterator をモデル化できませんでした .それは :'-(.

また、プロキシを返すイテレータも意味します — 参照のように振る舞う型 — ForwardIterator にすることはできません 秒。したがって、それが良い考えであったかどうかにかかわらず、 std::vector<bool> イテレータがプロキシを返すため、実際のコンテナではありません。

新しい C++20 イテレータの概念は、std::ranges::iter_swap の助けを借りて、この両方の問題を解決します。 (std::iter_swap の制約付きバージョン )、および新しい std::ranges::iter_move . ranges::iter_swap(i, j) を使用 i によって参照される値を交換する と j .そして、以下を使用してください:

iter_value_t<I> tmp = ranges::iter_move(i);

i の位置に要素を移動するには 順序が狂って、一時オブジェクト tmp に .

プロキシ イテレータ型の作成者は、これらの 2 つのカスタマイズ ポイントをフックして、イテレータが std::ranges の制約付きアルゴリズムでうまく機能するようにすることができます。 名前空間 (下記参照)。

新しい iter_rvalue_reference_t<I> 上記の型エイリアスは、ranges::iter_move(i) の戻り値の型を指定します .

連続イテレータのサポート

Stepanov の STL では、RandomAccessIterator 最強のイテレータ カテゴリです。しかし、要素が連続しているかどうか メモリ内の情報は有用な情報であり、その情報を利用してより効率的になるアルゴリズムが存在します。 Stepanov はそのことを認識していましたが、生のポインターが連続した反復子の唯一の興味深いモデルであると感じたため、新しいカテゴリを追加する必要はありませんでした。彼は std::vector を出荷するライブラリ ベンダーに愕然としたでしょう。 ラップされたデバッグ イテレータを使用した実装。

TL;DR、RandomAccessIterator を包含する (洗練する) 追加のカテゴリを定義しています。 ContiguousIterator と呼ばれる .型は、iterator_concept という名前のネストされた型を定義して、連続性をオプトインする必要があります (注:ではない iterator_category ) 新しい std::contiguous_iterator_tag のエイリアスです タグタイプ。または、std::iterator_traits を特化することもできます タイプに iterator_concept を指定します

制約付きアルゴリズム

std::list を渡そうとしたことがあります std::sort へのイテレータ ?またはナンセンスの他の組み合わせ?現在、アルゴリズムの (記述されていない) 型の要件を誤って満たすことができなかった場合、コンパイラは、STL 実装の内部から発生したと思われるエラーを吐き出し、可能な限り最もあいまいで大量の方法で通知します。

概念は、これを支援するように設計されています。たとえば、cmcstl2 参照実装を使用しているこのコードを見てください (これは std::ranges を置きます) std::experimental::ranges で 今のところ):

#include <list>
#include <stl2/algorithm.hpp>
using ranges = std::experimental::ranges;

int main() {
  std::list<int> l {82,3,7,2,5,8,3,0,4,23,89};
  ranges::sort( l.begin(), l.end() );
}

ranges::sort の根底にあるエラーではなく 、エラーメッセージは main の行を指しています sort の制約を満たさなかった テンプレート。 「エラー:ranges::sort(list<int>::iterator, list<int>::iterator) に一致する呼び出しがありません "、続いて、一致に失敗したプロトタイプを示すメッセージと、RandomAccessIterator 内の制約に関する説明が続きます 私たちは満足していません。ここで完全なエラーを確認できます。

エラーをよりユーザーフレンドリーにするためにできることはたくさんありますが、それはすでに現状から大幅に改善されています.

範囲アルゴリズム

これはかなり明白です。 STL が標準化されてから 20 年が経ちましたが、やりたいことは vector を渡すことだけです。 sort へ .それは聞きすぎですか?いいえ。 C++20 では、ついに これを行うことができます:

std::vector< int > v =  // ...
std::ranges::sort( v ); // Hurray!

制約付き関数オブジェクト

std::less<> を使ったことがありますか 、C++14 で追加された比較関数オブジェクトの「ダイヤモンド」特殊化?これらを使用すると、比較するタイプを前もって言ったり、変換を強制したりすることなく、物事を比較できます。これらは std::ranges に存在します 名前空間も同様ですが、<> と入力する必要はありません それらはテンプレートではないためです。また、制約付きの関数呼び出し演算子があります。だから lessgreaterless_equal 、および greater_equal すべて StrictTotallyOrderedWith で制限されています

これらのタイプは、ユーザー指定のリレーションを受け入れる API を定義する場合に特に便利ですが、リレーションのデフォルトは operator< です。 または operator== .例:

template <class T, Relation<T, T> R = ranges::less>
T max( T a, T b, R r = {} ) {
  return r( a, b ) ? b : a;
}

この関数には、ユーザーが関係を指定するとそれが使用され、制約によって R が保証されるという優れた特性があります。 Relation です オーバー タイプ T .ユーザーがしない場合 関係を指定すると、制約で T が必要になります StrictTotallyOrderedWith を満たす 自体。それは R という事実に暗示されています デフォルトは ranges::less です 、および ranges::less::operator() StrictTotallyOrderedWith で制約されます .

一般化された Callable

C++17 では、標準ライブラリに便利な関数が追加されました:std::invoke .いくつかの引数を使用して任意の「呼び出し可能な」ものを呼び出すことができます。「呼び出し可能な」には、メンバーへのポインターに加えて、通常の関数のようなものが含まれます。ただし、標準アルゴリズムは std::invoke を使用するように再指定されていません。 、つまり、次のようなコードはコンパイルに失敗しました:

struct Wizard {
  void frobnicate();
};

int main() {
  std::vector<Wizard> vw { /*...*/ };
  std::for_each( vw.begin(), vw.end(),
                 &Wizard::frobnicate ); // Nope!
}

std::for_each fun(t) のような呼び出し可能なものを期待しています 、std::invoke(fun, t) ではありません .

std::ranges の新しいアルゴリズム 名前空間は std::invoke を使用する必要があります 、したがって、上記のコードが std::ranges::for_each を使用するように変更された場合 、書かれているとおりに動作します。

予測

それらのもののいくつかのプロパティによって、さまざまなものを並べ替えたいと思ったことはありませんか?従業員のベクトルを ID で並べ替えますか?それとも苗字?あるいは、マグニチュードが特定の値に等しいポイントの配列を検索したいかもしれません。それらについては、予測 非常に便利です。射影は、アルゴリズムが要素に作用する前に各要素に適用されるアルゴリズムに渡される単項変換関数です。

従業員のベクトルを ID で並べ替える例を挙げると、std::ranges::sort への射影引数を使用できます。 次のように:

struct Employee {
  int Id;
  std::string Name;
  Currency Salary;
};

int main() {
  using namespace std;
  vector<Employee> employees { /*...*/ };
  ranges::sort( employees, ranges::less{},
                &Employee::Id );
}

std::ranges::sort への 3 番目の引数 が射影です。前のセクションから、一般化された callable を使用したことに注意してください。この sort コマンドは、従業員を Id で並べ替えます フィールド。

または、マグニチュードが特定の値に等しいポイントの配列を検索する例として、次のようにします:

using namespace std;
array< Point > points { /*...*/ };
auto it = ranges::find( points, value, [](auto p) {
  return sqrt(p.x*p.x + p.y*p.y);
} );

ここでは、射影を使用して各要素のプロパティを計算し、計算されたプロパティを操作しています。

プロジェクションのコツをつかめば、さまざまな用途があることがわかります。

範囲ユーティリティ

<ranges> で出荷される標準ライブラリの一部 ヘッダーにはたくさんの特典があります。遅延範囲アダプターの初期セット (以下で説明) に加えて、いくつかの便利な汎用ユーティリティがあります。

view_interface

上記のピタゴラス トリプルの例のように、カスタム ビュー タイプは view_interface から継承できます。 .front() のような便利なメンバー関数のホストを取得する 、 .back().empty().size().operator[] 、さらには bool への明示的な変換 ビュータイプが if で使用できるように ステートメント:

// Boolean conversion operator comes from view_interface:
if ( auto evens = vec | view::filter(is_even) ) {
  // yup, we have some evens. Do something.
}

部分範囲

std::ranges::subrange<I, S> 範囲ユーティリティの中でおそらく最も便利です。 View をモデル化するイテレータ/センチネルのペアです 概念。これを使用して、2 つのイテレーター、またはイテレーターとセンチネルをまとめて、範囲を返すか、範囲を予期する API を呼び出したい場合に使用できます。

また、使用するのが非常に簡単になる控除ガイドもあります。次のコードを検討してください:

auto [b,e] = subrange{vec};

このコードは実質的に次のものと同等です:

auto b = ranges::begin(vec);
auto e = ranges::end(vec);

subrange{vec} vec の範囲からイテレータとセンチネル テンプレート パラメータを推測します 、および subrange 以降 タプルに似ているため、構造化バインディングを使用してイテレータ/センチネルのペアをアンパックできます。

ref_view

まだ正式にマージされていませんが、C++20 には std::ranges::ref_view<R> が含まれます。 std::reference_wrapper のように まあ、参照のラッパーです。 ref_viewの場合 、範囲への参照です。 std::vector<int>& のような左辺値コンテナーになります View に 安価にコピーできる同じ要素のコピー:ベクトルへのポインターをラップするだけです。

範囲ジェネレーター

今、私たちは本当に楽しいものに行きます. <ranges> ヘッダーには、std::view::iota など、新しい範囲の値を生成する方法がいくつかあります 上で見たもの。それらの使用方法とその意味は次のとおりです。

構文 セマンティクス
view::iota(i) 増分可能なオブジェクト i を考えると 、 [i,i+1,i+2,i+3,...) のような無限の範囲の値を生成します .
view::iota(i,j) 増分可能なオブジェクト i を考えると およびその他のオブジェクト j これは i に匹敵します (必ずしも同じ型であるとは限りません)、[i,i+1,i+2,i+3,...,j-1] のような値の範囲を生成します .上限 (j ) は除外 、これにより、このフォームをイテレータ/センチネルのペアで使用できるようになります。 view::iota(0u, ranges::size(rng)) で範囲のインデックスを生成するためにも使用できます .
view::single(x) x の 1 要素ビューを作成します;つまり、[x] .
view::empty<T> タイプ T の要素のゼロ要素ビュー .
view::counted(it, n) 与えられたイテレータ it そしてカウント nn の有限範囲を構築します it で示される要素から始まる要素 .

レンジアダプター

これは本当に、本当に 楽しいもの。範囲の真の力は、その場で範囲を変換するパイプラインを作成する機能にあります。 range-v3 ライブラリには、多数の便利な範囲アダプターがあります。 C++20 はほんの一握りですが、時間の経過とともにセットが増えることを期待してください。

構文 セマンティクス
r | view::all View を作成する Range のすべての要素 r .おそらく r すでに View です .そうでない場合は、ref_view で 1 つに変換します 可能であれば、または subrange それに失敗します。右辺値コンテナーは「表示可能」ではないため、std::vector<int>{} | view::all のようなコード コンパイルに失敗します。
r | view::filter(pred) 可視範囲 r が与えられた場合 および述語 predView を返します すべての要素 e で構成される invoke(pred, e) true を返します .
r | view::transform(fn) 可視範囲 r を考える および関数 fnView を返します r のすべての要素で構成される fn で変換 .
r | view::reverse 可視範囲 r を考える 、View を返します r を繰り返す の値を逆順で。
r | view::take(n) 可視範囲 r を考える 、View を返します 最初の n を含む r の要素 、または r のすべての要素 r の場合 n 未満です
r | view::join 表示可能な範囲の範囲を指定して、すべての範囲を 1 つの範囲にフラット化します。
r | view::split(r2) 可視範囲 r を考える およびパターン範囲 r2View を返します View の 内部範囲が r2 で区切られている .または、区切り文字を単一の値 v にすることもできます view::single(v) であるかのように扱われます .
r | view::common 可視範囲 r が与えられた場合 、View を返します 範囲の開始イテレータと終了イテレータは同じ型です。 (一部の範囲では、終了位置にセンチネルを使用します。) この範囲アダプターは、主に古いコード (std:: など) とのインターフェイス手段として役立ちます。 begin と end が同じ型を持つことを期待するアルゴリズム)。

これらのアダプタは連鎖できるため、たとえば、次のことができます:

using namespace std;
for ( auto && e : r | view::filter(pred)
                    | view::transform(fn) ) {
  // Iterate over filtered, transformed range
}

もちろん、範囲アダプター パイプラインを std::ranges の範囲ベースのアルゴリズムへの引数として使用することもできます。 :

using namespace std;
// Insert a filtered, transformed range into
// the back of container `v`.
ranges::copy( r | view::filter(pred)
                | view::transform(fn),
              back_inserter(v) );

遅延適応範囲は、プログラムを構築するための強力な方法です。このプログラミング スタイルでどこまで行けるかを示すデモが必要な場合は、2015 年の範囲に関する私の CppCon 基調講演を参照するか、そこで説明しているカレンダー アプリケーションのコードをざっと読んで、ループ、分岐、明白な状態操作がないことに注意してください。 . 「ナフは言った。

今後の方向性

明らかに、C++20 は多く増えています。 範囲をサポートする新機能。ここまで来るのに長い時間がかかりました。その主な理由は、概念の C++20 言語サポートを使用して、完全に一般的で工業的な強度のジェネリック ライブラリを構築した人がこれまで誰もいなかったからです。しかし今、私たちはその困難を乗り越えました。すべての基本的な要素が整っており、その過程で多くの知識が得られました。 C++20 以降、機能セットが急速に拡大することが予想されます。すでに進行中の論文があります。

現在進行中の内容:

  • 範囲を受け入れる標準コンテナのコンストラクタ
  • A take_while 述語を受け入れ、最初の N のビューを返す範囲アダプター 述語が true と評価される要素 、
  • A drop 最初の N を削除した後にビューを返す範囲アダプター 入力範囲の要素
  • A drop_while 述語を満たす入力範囲から要素を削除するビュー。
  • istream_view 型でパラメータ化され、標準の istream からその型の要素を読み取る 、
  • A zip N かかるビュー 要素が N であるビューを生成します -入力範囲の要素のタプル、および
  • A zip_with N かかるビュー 範囲と N -ary 関数を使用して、要素が入力範囲の要素で関数を呼び出した結果であるビューを生成します。

そして、range-v3 にはさらに多くの機能があり、有用であることが証明されており、最終的には私自身または他の関心のある range-r によって提案される予定です。特に見たいもの:

  • range-v3 の basic_iterator のようなイテレータ ファサード クラス テンプレート;
  • range-v3 の view_facade のようなビュー ファサード クラス テンプレート;
  • 数値アルゴリズムの範囲化されたバージョン (例:accumulatepartial_suminner_product );
  • view::chunk などの範囲ジェネレーターとアダプターの追加 、 view::concatview::group_byview::cycleview::sliceview::strideview::generate[_n]view::repeat[_n]view::join 区切り文字 view::intersperse を取ります 、 view::unique 、および view::cartesian_product 、より重要なものに名前を付けます。そして
  • アクションの「完全な」セット 景色に合わせて。 view:: のアダプターのようなアクション 名前空間、範囲で動作し、パイプラインに構成しますが、アクションは熱心に動作します コンテナ全体で、潜在的に変異しています。 (ビューは変化しません。)

アクションを使用すると、次のことが可能になるはずです:

v = move(v) | action::sort | action::unique;

…ベクトルを並べ替え、すべての重複要素を削除します。

非同期範囲についても触れていません まだ。しかし、それはまったく別のブログ投稿です。 🙂

まとめ

C++20 が急速に近づいており、Ranges の作業が正式にワーキング ドラフトにマージされたので、これらすべての実装を検討し始めている標準ライブラリ ベンダーから聞いています。概念をサポートして現在出荷されている唯一のコンパイラであるため、すぐに範囲サポートを出荷できる立場にあるのは GCC だけです。しかし、clang にはすでに使用可能なコンセプト ブランチがあるため、それほど遠くない将来のいつか、clang トランクにコンセプト (および範囲) の希望があります。 Microsoft は すべて をサポートすることを公に約束しています C++20 の概念と範囲を含め、Microsoft コンパイラの適合性は急速に改善されており、最近では range-v3 をコンパイルできるようになりました。そこでも調子が良さそうです。

それは奇妙な新しい世界です。読んでくれてありがとう。

"\e"