[C++] <ranges>のviewを見る15 - common_view

4 min read読了の目安(約3900字

common_view

common_viewは元となるシーケンスを、begin()/end()によって取得できるイテレータと終端イテレータの型が同じとなるシーケンスに変換するViewです。

int main() {
  auto even_seq = std::views::iota(1)
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::take(10);

  // イテレータ型と終端イテレータ型が合わないためエラー 
  std::vector<int> vec(std::ranges::begin(even_seq), std::ranges::end(even_seq)); // ng
  
  std::ranges::common_view common{even_seq};
  
  std::vector<int> vec(std::ranges::begin(common), std::ranges::end(common)); //ok

  for (int n : vec) {
    std::cout << n; // 2468101214161820
  }
}

これはC++20以降のイテレータ(rangeライブラリのViewなどのイテレータ)をC++17以前のイテレータを受け取るものに渡す際に使用します。

標準コンテナのイテレータペアを受け取るコンストラクタや<algorithm>の各種アルゴリズム群など、C++17以前のイテレータはbegin()/end()の型が同一である事を前提としています。しかし、C++20以降のイテレータおよびrangeは異なっていることが前提です。特に、各種Viewクラスの場合は元となるrangeの種別や構築のされ方によってbegin()/end()の型は細かく変化するので、古いライブラリと組み合わせる際にcommon_viewは必須でしょう。

<algorithm>のイテレータアルゴリズム関数群はstd::ranges名前空間の下にある同名の関数を利用すればC++20以降のイテレータに対してもそのまま使用できるようになっていますが、標準コンテナのイテレータペアを取るコンストラクタや<numeric>にあるアルゴリズム関数などではcommon_viewを使用する必要があります。

int main() {
  auto even_seq = std::views::iota(1)
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::take(10);

  auto common = std::views::common(even_seq);
  
  // 古いやつ
  auto it1 = std::find_if(common.begin(), common.end(), [](int n) { return 10 < n;});
  std::cout << *it1 << '\n';  // 12
  
  // 新しいやつ
  auto it2 = std::ranges::find_if(even_seq.begin(), even_seq.end(), [](int n) { return 10 < n;});
  std::cout << *it2 << '\n';  // 12
  
  // むしろrange直接
  auto it3 = std::ranges::find_if(even_seq, [](int n) { return 10 < n;});
  std::cout << *it3 << '\n';  // 12

  // 古いやつ
  auto sum = std::accumulate(common.begin(), common.end(), 0u);
  std::cout << sum << '\n';   // 110
  
  // まだない・・・
  //auto sum2 = std::ranges::accumulate(even_seq.begin(), even_seq.end(), 0u);
  //auto sum3 = std::ranges::accumulate(even_seq, 0u);
}

common_viewは元となるrangeforward_range以上であればforward rangeとなり、それ以外の場合はinput rangeとなります。

common_range

begin()/end()の型が同一であるrangestd::ranges::common_rangeコンセプトによって表現され、そのようなrangecommon rangeと呼びます。

template<class T>
concept common_range =
  range<T> && same_as<iterator_t<T>, sentinel_t<T>>;

C++20以降のrange/イテレータライブラリにおいては、begin()から得られるものをイテレータ、end()から得られる終端イテレータの事を番兵(Sentinel)と呼び分けます。Sentinelという言葉は既にrangeライブラリ周りでは当たり前のように使われています、慣れましょう。

views::common

common_viewに対応するrange adaptor objectstd::views::commonです。

#include <ranges>

int main() {
  auto even_seq = std::views::iota(1)
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::take(10);

  auto common1 = std::views::common(even_seq);
  
  std::vector<int> vec(std::ranges::begin(common1), std::ranges::end(common1)); //ok


  for (int n : vec) {
    std::cout << n; // 2468101214161820
  }
}
#include <ranges>

int main() {
  // パイプラインスタイル
  auto even_seq = std::views::iota(1)
    | std::views::filter([](int n) { return n % 2 == 0; })
    | std::views::take(10)
    | std::views::common;

  std::vector<int> vec(std::ranges::begin(even_seq), std::ranges::end(even_seq)); //ok
  
  
  for (int n : vec) {
    std::cout << n; // 2468101214161820
  }
}

views::commonはカスタマイゼーションポイントオブジェクトであり、rangeオブジェクト(r)を1つ受け取り、それがすでにcommon_rangeならばstd::views::all(r)の結果を、common_rangeでないならばrを転送してcommon_viewを構築して返します。

結果の型を区別しなければ、あらゆるrangeに対してcommon_view相当のものを得ることができます。特にcommon_viewcommon_rangeから構築することができないので、common_viewが欲しい際は常にviews::commonを利用するとよりジェネリックです。