⛓️

[C++] <ranges>のviewを見る17 - elements_view

2020/10/30に公開

elements_view

elements_viewtuple-likeな型の値のシーケンスから、指定された番号の要素だけからなるシーケンスを生成するViewです。

#include <ranges>

int main() {
  // tupleのシーケンス
  std::vector<std::tuple<int, double, std::string_view>> vec = { {1, 1.0, "one"}, {2, 2.0, "two"}, {3, 3.0, "three"} };
  
  // 1つ目(int)のtuple要素のシーケンス
  std::ranges::elements_view<std::views::all_t<decltype((vec))>, 0> ev1{vec};
  
  for (int n : ev1) {
    std::cout << n; //123
  }
  
  std::cout << '\n';

  // 3つ目(string_view)のtuple要素のシーケンス
  std::ranges::elements_view<std::views::all_t<decltype((vec))>, 2> ev2{vec};
  
  for (std::string_view sv : ev2) {
    std::cout << sv << ' '; // one two three
  }
}

tuple-likeな型のシーケンスのそれぞれの要素を指定された番号によるget<N>()で射影し、その値のシーケンスを生成します。

elements_viewには抽出するtuple要素の番号を非型テンプレートパラメータとして渡さなければならないため、クラステンプレートの実引数推定を利用できません。そのため、引数として渡すrangeの型を書く必要があります。
std::views::all_tというのはviews::allの結果を示す型エイリアスで、これを通すことによってViewが直接rangeを所有・参照する事を回避します(これはref_viewを除く他のViewでも同様です)。decltype(())としているのはviews::allに左辺値として渡すためです。

これは特に、連想コンテナにおいて便利かと思われます。

#include <ranges>

int main() {
  using namespace std::string_view_literals;

  std::map<int, std::string_view> i2s = { {6, "six"sv}, {1, "one"sv}, {0, "zero"sv}, {2, "two"sv}, {3, "three"sv}, {5, "five"sv}, {4, "four"sv} };
  
  // keyのシーケンス
  std::ranges::elements_view<std::views::all_t<decltype((i2s))>, 0> ev1{i2s};
  
  for (int n : ev1) {
    std::cout << n; // 0123456
  }
  
  std::cout << '\n';

  // valueのシーケンス
  std::ranges::elements_view<std::views::all_t<decltype((i2s))>, 1> ev2{i2s};
  
  for (std::string_view sv : ev2) {
    std::cout << sv << ' '; // zero one two three four five six
  }
}

std::tuplegetを使用する時と同様に、指定する要素番号は0始まりでなければなりません。

遅延評価

elements_viewによるシーケンスもまた、遅延評価によって生成されます。とはいえそれは単純に、イテレータの間接参照のタイミングで元のシーケンスの要素に対してget<N>を適用して要素を取り出すだけです。

std::vector<std::tuple<int, double, std::string_view>> vec = { {1, 1.0, "one"}, {2, 2.0, "two"}, {3, 3.0, "three"} };
  
// elements_view構築時は何もしない
std::ranges::elements_view<std::views::all_t<decltype((vec))>, 0> ev1{vec};

// イテレータ取得時も何もしない
auto it = std::ranges::begin(ev1);

// インクリメント等進行操作はほぼ元のイテレータそのまま
++it;
--it;

// 間接参照時に元のイテレータの間接参照結果にget<N>を適用して1要素だけを取り出す
auto&& elem = *it;

elements_viewが行う殆どの事はそのイテレータの間接参照時に集中しており、そのほかの操作は元のイテレータの薄いラッパとなります。そのため、元のイテレータの性質をほぼそのまま受け継ぎ、elements_viewrangeカテゴリは元のrangeと同じになります。

views::elements

elements_viewに対応するrange adaptor objectstd::views::elementsです。

#include <ranges>

int main() {
  std::vector<std::tuple<int, double, std::string_view>> vec = { {1, 1.0, "one"}, {2, 2.0, "two"}, {3, 3.0, "three"} };
  
  for (int n : std::views::elements<0>(vec)) {
    std::cout << n; //123
  }
  
  std::cout << '\n';
  
  // パイプラインスタイル
  for (std::string_view sv : vec | std::views::elements<2>) {
    std::cout << sv << ' '; // one two three
  }
}

views::elementsはカスタマイぜーションポイントオブジェクトであり、テンプレートパラメータとして抽出するtuple要素の番号を、引数としてtuple-likeな型のシーケンスを受け取り、それらによってelements_viewを構築して返します。
elements_viewは要素番号をテンプレートパラメータで受け取る都合上クラステンプレートの実引数推論が効かないため、テンプレートパラメータに引数rangeの型を書かなければなりませんが、views::elementsを使うことでそれを省略できます。

keys_view/values_view

elements_viewは連想コンテナにおいてよく使用される事を想定しているためか、連想コンテナで使う際に便利なエイリアスがあらかじめ用意されています。これを用いると、冒頭の連装コンテナのサンプルは次のように書くことができます。

#include <ranges>

int main() {
  using namespace std::string_view_literals;

  std::map<int, std::string_view> i2s = { {6, "six"sv}, {1, "one"sv}, {0, "zero"sv}, {2, "two"sv}, {3, "three"sv}, {5, "five"sv}, {4, "four"sv} };
  
  // keyのシーケンス
  std::ranges::keys_view ev1{i2s};
  
  for (int n : ev1) {
    std::cout << n;
  }
  
  std::cout << '\n';

  // valueのシーケンス
  std::ranges::values_view ev2{i2s};
  
  for (std::string_view sv : ev2) {
    std::cout << sv << ' ';
  }
}

keys_view/values_viewelements_viewのエイリアステンプレートで、次のように定義されます。

namespace std::ranges {
  template<typename R>
  using keys_view = elements_view<views::all_t<R>, 0>;

  template<typename R>
  using values_view = elements_view<views::all_t<R>, 1>;
}

C++20からはエイリアステンプレートの実引数推論が導入されているので、これらを利用すると完全に型を省略できるようになります(GCCは未対応だったためWandboxでは型を書いていますが・・・)。

さらに、keys_view/values_viewに対応するrange adaptor objectも用意されています。

#include <ranges>

int main() {
  using namespace std::string_view_literals;

  std::map<int, std::string_view> i2s = { {6, "six"sv}, {1, "one"sv}, {0, "zero"sv}, {2, "two"sv}, {3, "three"sv}, {5, "five"sv}, {4, "four"sv} };
  
  // keyのシーケンスをイテレート
  for (int n : i2s | std::views::keys) {
    std::cout << n;
  }
  
  std::cout << '\n';

  // valueのシーケンスをイテレート
  for (std::string_view sv : i2s | std::views::values) {
    std::cout << sv << ' ';
  }
}

view::keys/views::valuesはカスタマイゼーションポイントオブジェクトであり、2要素以上のtuple-likeオブジェクトによるrangeを受け取ってkeys_view/values_viewを構築して返します。
これらを用いると、さらに意図を明確に書くことができます。

おわりに

これをもってC++20 rangeライブラリのViewの遊覧は終わりとなります、お疲れ様でした。

他のモダンな言語でのシーケンス操作に触れたことがある人には、C++20 rangeライブラリのViewは物足りなく写ることでしょう。全部で17個しかないViewの中には、例えばzipconcat等のよく利用されるものが含まれていませんから・・・

とはいえ、rangeライブラリはこれで終わりではありません。rangeライブラリのほとんどの部分はrange-v3というライブラリでの経験をベースにしています(そもそも、提案者がその作者のEric Nieblerさんだったりします)が、それは巨大なライブラリであり、そのすべてを一度にC++に導入しようとすると膨大な作業が必要となります。ともすれば、十分な議論を尽くすことができないかもしれません。

そのため、C++20ではrangeライブラリの基礎となるコンセプトとユーティリティ、及び基本的なViewだけに範囲を絞ったうえで提案されています。C++20のrangeライブラリが物足りないのはあえてのことです。

C++23に向けては既にいくつかのViewの提案が出されていますが、本格的にrangeライブラリの拡張が始まる予定です。そこにはViewだけではなく、rangeに対するAlgorithmaccumulate, inner_productなどの様な操作)やActionrangeに直接作用するsort, copyなどの様な操作)も含まれています。P2214R0 A Plan for C++23 Rangesに展望が描かれています。

とは言えやはり、C++は長期感使われることを見越した上で機能が安定していることを重視しており、議論は慎重に十分な時間をかけて行われます。そのため一度に全部とはいかず、優先度を付けながら、すこしづつ新しいものを導入していく事になります。

もし生きていたら、C++23でお会いしましょう。

Discussion