C++23 <ranges>のviewを見る2 - As rvalue view
views::as_rvalue
views::as_rvalueは、入力シーケンスの各要素をstd::move()した右辺値の要素からなるシーケンスを生成するviewです。
import std;
int main() {
std::vector<std::string> strvec = { "move_view", "all_move_view", "as_rvalue_view" };
for (std::string&& rv : strvec | std::views::as_rvalue) {
std::ptrintln("{:s}", rv);
}
// move_view
// all_move_view
// as_rvalue_view
}
このサンプルコードはほぼ意味がなく、これはC++23のranges::toとともに使用されることを意図しています。
import std;
int main() {
std::vector<std::unique_ptr<int>> upvec;
upvec.emplace_back(std::make_unique<int>(10));
upvec.emplace_back(std::make_unique<int>(100));
upvec.emplace_back(std::make_unique<int>(17));
// listに詰め替え
auto up_list = upvec | std::views::as_rvlaue
| std::ranges::to<std::list>; // ok
// upvecの要素はunique_ptrの左辺値であるため、これだとコピーしようとしてエラー
auto up_list = upvec | std::ranges::to<std::list>; // ng
}
ranges::toに関してはこちらを参照
同様の事をRangeアルゴリズムでやる時にも使用できます。
import std;
int main() {
std::vector<std::unique_ptr<int>> upvec;
upvec.emplace_back(std::make_unique<int>(10));
upvec.emplace_back(std::make_unique<int>(100));
upvec.emplace_back(std::make_unique<int>(17));
// listに詰め替え
std::list<std::unique_ptr<int>> uplist;
std::ranges::copy(upvec | std::views::as_rvalue, std::back_inserter(uplist));
}
Rangeアダプタオブジェクト
views::as_rvalueはRangeアダプタオブジェクトであり、入力範囲rによってviews::as_rvalue(r)のように呼ばれた時
-
rの要素型が右辺値である時 :views::all(r)を返す- 正確には、
rの右辺値参照型(range_rvalue_reference_t)とrの参照型(range_reference_t)が同じ型である時
- 正確には、
- そうではない(
rの要素型が左辺値である)時 :as_rvalue_view{r}を返す
この場合のrはその値カテゴリに応じて適切にムーブ/コピーされます。
views::as_rvalueは入力範囲の要素の値カテゴリを見て、それが既に右辺値の場合は何もせず、左辺値である場合にのみas_rvalue_view{r}を返します。
これによって、既に右辺値の範囲となっている入力範囲に対して要素ごとにstd::moveをかけていくことを回避しており、特に間接参照結果がprvalueになっている範囲に対して使用した場合にコピー省略を妨げないようになっています。
import std;
int main() {
std::string str = "move, all_move, as_rvalue";
// 入力文字列を', 'で分割して、それに何かを付け加えて、stringのvectorに詰める
auto strvec = str | std::views::split(std::string_view{", "})
| std::views::transform([](auto substr) -> std::string {
return "views::" + std::string(std::from_range, substr);
})
| std::views::as_rvalue // 何もしない
| std::ranges::to<std::vector>;
// 2行目のtransformによってstd::stringのprvalueの範囲となっており
// as_rvalueは何もせず、最後のranges::toによるvectorへの挿入ギリギリまでprvalueは実体化しない
for (auto& str : strvec) {
std::println("{:s}", str);
}
}
as_rvalue_view
as_rvalue_viewはviews::as_rvalueが入力範囲を変換する場合の実装詳細です。とはいえ複雑なものではなく、実体はほとんどmove_iteratorに委譲されています。
// as_rvalue_viewの宣言例
namespace std::ranges {
template<view V>
requires input_range<V>
class as_rvalue_view : public view_interface<as_rvalue_view<V>> {
...
// 非const begin()
constexpr auto begin() requires (!simple-view<V>)
{ return move_iterator(ranges::begin(base_)); }
// 非const end()
constexpr auto end() requires (!simple-view<V>) {
if constexpr (common_range<V>) {
return move_iterator(ranges::end(base_));
} else {
return move_sentinel(ranges::end(base_));
}
}
...
};
}
as_rvalue_viewの存在意義は入力範囲をviews::allに通して保持しておくところにあります。例えば、move_iteratorとsubrangeでも同じようなものを構成できますが、これだと入力のrangeオブジェクトがダングリングになることがあり、Rangeアダプタとして使用される場合にそれは致命的となるでしょう。
views::as_rvalueの各種特性
入力の範囲Vの要素型をTとすると、Tが右辺値である場合は元の範囲と同じ性質を保ち、Tが左辺値である(as_rvalue_view<V>が使用される場合)場合は次のようになります
-
reference:T&& -
rangeカテゴリ :input_range -
common_range:Vがcommon_rangeである時 -
sized_range:Vがsized_rangeである時 -
const-iterable:Vがconst-iterableである時 -
borrowed_range:enable_borrowed_range<V>がtrueである時
C++17までの(C++17イテレータとしての)move_iterator<I>はラップするイテレータIの性質をなるべく再現しようとしていました。しかし、move_iterator<I>の各要素は対応するIのイテレータの要素をムーブしたものであり、同じ要素への2回目以降のアクセスは安全ではありません。
そのため、C++20からの(C++20イテレータとしての)move_iterator<I>はIにかかわらず常にinput_iteratorとなります。これを受けて、as_rvalue_viewも常にinput_rangeとなります。
Discussion