[C++] <ranges>のviewを見る5 - ref_view

5 min読了の目安(約4700字TECH技術記事

range adaptors

range adaptorsは他のViewを含む任意のrangeに対して作用して、特定の操作を適用したViewに変換するものです。range adaptorrangeからrangeへ操作を適用しつつ変換するものなので、range adaptorの結果にさらにrange adaptorを適用する形で操作をチェーンさせることができます。そして、その最終的な結果もまたrangeとして得られます。

range factoriesはシーケンスを生成するタイプのViewなのでrange adaptorsのように他のrangeに作用することはできませんが、range adaptorsと比較してみるとrange factoriesrange adaptorsによるチェーンの起点となるViewであることが分かるでしょう。

range adaptor objects

range factoriesView型にはstd::views名前空間にその構築を簡略化するための関数オブジェクトなどが用意されていました。これと同様に、range adaptorsにもその構築を簡略化し明瞭にするための関数オブジェクトが用意されます。これらのものはrange adaptor objectsと呼ばれます。

range adaptor objectsは第一引数にrangeを取り、戻り値として対応するViewを返すカスタマイゼーションポイントオブジェクトとして定義されています。

パイプライン演算子(|)と関数呼び出し

range adaptorは関数呼び出しによって使用するほかに、パイプライン演算子(|)によっても使用することができます。パイプライン演算子によるスタイルはネストした関数呼び出しをその適用順に分解した形で書くことができ、コードの可読性の向上が期待できます。

同じrange adaptorについては、パイプラインスタイルと関数呼び出しスタイルはどちらを用いても同じViewが得られることが保証されています。

// R, R1, R2を任意のrange adaptorとする

// この2つの呼び出しは同じViewを返す
R(std::views::iota(1));
std::views::iota(1) | R ;

// この3つの呼び出しも同じViewを返す、さらにrange adaptorが増えても同様
R2(R1(std::views::iota(1)));
std::views::iota(1) | R1 | R2;
std::views::iota(1) | (R1 | R2);

// range adopterが追加の引数を取るときでも、次の3つは同じViewを生成する
R(std::views::iota(1), args...);
R(std::views::iota(1))(args...);
std::views::iota(1) | R(args...)

なお、このパイプライン演算子は新しい演算子ではなくて既存のビット論理和演算子をオーバーロードしたものです。

ref_view

ref_viewは他のrangeを参照するだけのViewです。

#include <ranges>

int main() {
  std::vector vec = {1, 3, 5, 7, 9, 11};

  for (int n : std::ranges::ref_view(vec)) {
    std::cout << n; // 1357911
  }
}

右辺値でさえなければ任意のrangeを受けることができて、そのrangeを単に参照するだけのViewとなります。ref_viewrangeのカテゴリは参照しているrangeのものを受け継ぎます。

ぱっと見ると何に使うのか不明ですが、これはstd::vectorなどのコピーが気軽にできないrangeの取り回しを改善するために利用できます。そのようなrangeの軽量な参照ラッパとなることで、コピーされるかを気にしなくてよくなるなど可搬性が向上します。役割としては、通常のオブジェクトの参照に対するstd::reference_wrapperに対応しています。
例えば、std::asyncの追加の引数として渡すときなどのようにdecay copyされてしまう所に渡す場合に活用できるでしょう。

struct check {
  check() = default;
  check(const check&) {
    std::cout << "コピーされたよ!" << std::endl;
  }
};

int main()
{
  std::vector<check> vec{};
  vec.emplace_back();
  
  [[maybe_unused]]
  auto f1 = std::async(std::launch::async, [](auto&&) {}, vec); // vectorがコピーされる
  
  [[maybe_unused]]
  auto f2 = std::async(std::launch::async, [](auto&&) {}, std::ranges::ref_view{vec});  // ref_viewがコピーされる
}

また、C++20のRangeライブラリの元となったRange-V3ライブラリではzip_viewを構成するためにも利用されているようです。
例えば、Viewを作ろうとするとデフォルト構築とムーブ構築/代入が少なくとも求められますが、rangeへの参照を持つと代入演算子やデフォルトコンストラクタが定義ができなくなり、ポインタを利用する場合はnullptrを気にしなければなりません。viewコンセプトによるViewの定義を思い出すと、すべてのViewはデフォルト構築可能でムーブ構築/代入が可能であり、ref_viewもまたそれに従います。
このように自分でViewを作成する時など、他のrangeをクラスメンバに持って参照したいときに直接そのrangeの参照を持つ代わりに利用することもできます。

比較的短いので定義も見てみましょう。

namespace std::ranges {
  template<range R>
    requires is_object_v<R>
  class ref_view : public view_interface<ref_view<R>> {
  private:
    R* r_ = nullptr;  // 説明専用メンバ変数
  public:
    constexpr ref_view() noexcept = default;

    template<not-same-as<ref_view> T>
      requires /*see below*/
    constexpr ref_view(T&& t);

    constexpr R& base() const { return *r_; }

    constexpr iterator_t<R> begin() const { return ranges::begin(*r_); }
    constexpr sentinel_t<R> end() const { return ranges::end(*r_); }

    constexpr bool empty() const
      requires requires { ranges::empty(*r_); }
    { return ranges::empty(*r_); }

    constexpr auto size() const requires sized_­range<R>
    { return ranges::size(*r_); }

    constexpr auto data() const requires contiguous_­range<R>
    { return ranges::data(*r_); }
  };

  template<class R>
    ref_view(R&) -> ref_view<R>;
}

このように、ref_view自体は対象のrangeへのポインタを保持し、参照するrangeのイテレータをそのまま利用します。

views::all

ref_viewに対応するrange adaptor objectstd::views::allです。

#include <ranges>

int main() {
  std::vector vec = {1, 3, 5, 7, 9, 11};

  for (int n : std::views::all(vec)) {
    std::cout << n;
  }

  std::cout << '\n';

  // パイプラインスタイル
  for (int n : vec | std::views::all) {
    std::cout << n;
  }
}

views::allはカスタマイゼーションポイントオブジェクトであり、1つの引数(r)を受け取りそれに応じて次の3つのいずれかの結果を返します。

  1. rViewである( std::decay_t<decltype(r)>std::ranges::viewコンセプトを満たす)ならば、rdecay copyして返す
  2. ref_view{r}が構築可能ならば、ref_view{r}
  3. それ以外の場合、std::ranges::subrange{r}

厳密にはref_viewだけを生成するわけではないのですが、結果の型を区別しなければ実質的にref_view相当のViewを得ることができます。

参考文献