[C++] <ranges>のviewを見る17 - elements_view
elements_view
elements_view
はtuple-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::tuple
にget
を使用する時と同様に、指定する要素番号は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_view
のrangeカテゴリは元のrangeと同じになります。
views::elements
elements_view
に対応するrange adaptor objectがstd::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_view
はelements_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の中には、例えばzip
やconcat
等のよく利用されるものが含まれていませんから・・・
とはいえ、rangeライブラリはこれで終わりではありません。rangeライブラリのほとんどの部分はrange-v3というライブラリでの経験をベースにしています(そもそも、提案者がその作者のEric Nieblerさんだったりします)が、それは巨大なライブラリであり、そのすべてを一度にC++に導入しようとすると膨大な作業が必要となります。ともすれば、十分な議論を尽くすことができないかもしれません。
そのため、C++20ではrangeライブラリの基礎となるコンセプトとユーティリティ、及び基本的なViewだけに範囲を絞ったうえで提案されています。C++20のrangeライブラリが物足りないのはあえてのことです。
C++23に向けては既にいくつかのViewの提案が出されていますが、本格的にrangeライブラリの拡張が始まる予定です。そこにはViewだけではなく、rangeに対するAlgorithm(accumulate, inner_product
などの様な操作)やAction(rangeに直接作用するsort, copy
などの様な操作)も含まれています。P2214R0 A Plan for C++23 Rangesに展望が描かれています。
とは言えやはり、C++は長期感使われることを見越した上で機能が安定していることを重視しており、議論は慎重に十分な時間をかけて行われます。そのため一度に全部とはいかず、優先度を付けながら、すこしづつ新しいものを導入していく事になります。
もし生きていたら、C++23でお会いしましょう。
Discussion