➡️

C++23 <ranges>のviewを見る2 - As rvalue view

2023/04/01に公開

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の要素型が左辺値である)時 : 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_viewviews::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_iteratorsubrangeでも同じようなものを構成できますが、これだと入力のrangeオブジェクトがダングリングになることがあり、Rangeアダプタとして使用される場合にそれは致命的となるでしょう。

views::as_rvalueの各種特性

入力の範囲Vの要素型をTとすると、Tが右辺値である場合は元の範囲と同じ性質を保ち、Tが左辺値である(as_rvalue_view<V>が使用される場合)場合は次のようになります

  • reference : T&&
  • rangeカテゴリ : input_range
  • common_range : Vcommon_rangeである時
  • sized_range : Vsized_rangeである時
  • const-iterable : Vconst-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