C++20 Rangesライブラリの逆順ビューstd::ranges::reverse_view
およびRangeアダプタstd::views::reverse
*1についてメモ。
基本の使い方
範囲for構文とRangeアダプタreverse
を組み合わせて、配列や文字列やコンテナなどRangeとして扱えるものを逆順に列挙できる。*2 *3
#include <iostream> #include <map> #include <string> #include <string_view> #include <ranges> using namespace std::literals; // ""sv int a[] = {1, 2, 3, 4, 5}; for (int n: a | std::views::reverse) { std::cout << n; // 54321 } for (char c: "Hello"sv | std::views::reverse) { std::cout << c; // olleH } // 文字列リテラル "Hello" に適用すると終端NUL文字を含む6文字が // '\0', 'o', 'l', 'l', 'e', 'H' 順で列挙されることに注意。 std::map<int, std::string> m = {{1, "apple"}, {2, "banana"}, {3, "cinnamon"}}; for (const auto& [k, v]: m | std::views::reverse) { std::cout << k << ":" << v << '\n'; // 3:cinnammon // 2:banana // 1:apple }
対象を逆順走査するには、そのイテレータが双方向(bidirectional)走査可能である必要がある。例えば単方向リストstd::forward_list
のイテレータは双方向走査をサポートしないため、reverse
と組み合わせるとコンパイルエラーとなる。膨大なエラーメッセージと共に_(:3 」∠)_*4
#include <forward_list> #include <ranges> std::forward_list lst = {1, 2, 3, 4, 5}; // std::forward_list は forward_range だが bidirectional_range ではない static_assert( !std::ranges::bidirectional_range<decltype(lst)> ); for (int n: lst | std::views::reverse) { // NG: ill-formed // ... }
応用例
Range分割アダプタstd::views::split
の適用後は Forward Range となり、下記コードのように直接reverse
と組み合わせてもコンパイルエラーになる。これはC++20 Rangesライブラリは遅延(lazy)評価戦略をとるため、分割処理が前方向(forward)にのみ行われることに起因する。
#include <iostream> #include <string_view> #include <ranges> std::string_view str = "apple,banana,cinnamon"; // NG: カンマ(,)区切りで分割後にアイテム名を逆順表示 for (auto item: str | std::views::split(',') | std::views::reverse) { // ill-formed std::cout << item << '\n'; }
次期C++2b(C++23)向けの提案文書P2210R2は欠陥修正としてC++20へも遡及適用されるため、C++20標準ライブラリ+P2210R2適用済みであれば下記コードで所望の動作となる*5。さらにC++2b標準ライブラリであれば、式(分割後subrange) | std::views::reverse
からstd::string_view
を直接構築できるようになる。(→id:yohhoy:20210624)
// C++20 + P2210R2 // OK: カンマ(,)区切りで分割後にアイテム名を逆順表示 for (auto rev_item: str | std::views::reverse | std::views::split(',')) { // rev_itemは nomannic, ananab, elppa と列挙されるため // 個別にreverseを適用して元の文字並び順に復元する auto item = rev_item | std::views::reverse; // coutストリーム出力を行うには部分範囲itemの型 // subrange<const char*, const char*, subrange_kind::sized> // から文字列型std::stringへの明示変換が必要となる std::cout << std::string{item.begin(), item.end()} << '\n'; } // cinnamon // banana // apple
// C++2b(C++23) // OK: カンマ(,)区切りで分割後にアイテム名を逆順表示 for (auto rev_item: str | std::views::reverse | std::views::split(',')) { std::cout << std::string_view{rev_item | std::views::reverse} << '\n'; } // OK: カンマ分割結果を逆順にstring_view化するRangeアダプタ auto split_reverse = std::views::reverse | std::views::split(',') | std::views::transform([](auto rv){ return std::string_view{rv | std::views::reverse}; }); for (auto item: str | split_reverse) { std::cout << item << '\n'; }
C++20 24.7.14.1/p1-2より引用。
1
reverse_view
takes a bidirectionalview
and produces anotherview
that iterates the same elements in reverse order.
2 The nameviews::reverse
denotes a range adaptor object (24.7.1). Given a subexpressionE
, the expressionviews::reverse(E)
is expression-equivalent to:
- If the type of
E
is a (possibly cv-qualified) specialization ofreverse_view
, equivalent toE.base()
.- Otherwise, if the type of
E
is cv-qualifiedsubrange<reverse_iterator<I>, reverse_iterator<I>, K>
for some iterator typeI
and valueK
of typesubrange_kind
,
- if
K
issubrange_kind::sized
, equivalent to:subrange<I, I, K>(E.end().base(), E.begin().base(), E.size())
- otherwise, equivalent to:
subrange<I, I, K>(E.end().base(), E.begin().base())
However, in either case
E
is evaluated only once.
- Otherwise, equivalent to
reverse_view{E}
.
関連URL
*1:完全修飾名は std::ranges::views::reverse だが標準ヘッダ<ranges>内で namespace std { namespace views = ranges::views; } と宣言されており、std::views::reverse とも記述できる。
*2:厳密には View として扱えるものを対象とする。View は Range の一種であり、コピー/ムーブ操作や破棄操作を定数時間で行えるものが View と定義される(C++20 24.4.4)。両者には構文上の差異がないため、ユーザ定義型向けのカスタマイズポイントとして変数テンプレート std::range::enable_view<T> が提供される。
*3:例示コードではRangeアダプタ適用によるクラステンプレート型推論過程で int[5] は std::ranges::ref_view<int [5]> へ、std::map<int, std::string> は std::ranges::ref_view<std::map<int, std::string>> へと View に自動変換されている。2例目の std::basic_string_view は字面通りそのまま View とみなされる。仮に std::string とした場合は std::ranges::ref_view<std::string> へ変換される。
*4:https://gist.github.com/yohhoy/4ff16f93d62e35be3788c5cdca5858d0
*5:R2210R2修正前の場合、split適用後の内部イテレータは Forward Iterator となりそのままでは逆順走査を行えない。一旦バッファリングしてから文字列反転させる必要がある。実装例:https://gist.github.com/yohhoy/5f104e6d3aee2b3e927419664abf63af