Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rangeアダプタを作る

Tetsuro Matsumura
December 21, 2024
180

 Rangeアダプタを作る

Tetsuro Matsumura

December 21, 2024
Tweet

Transcript

  1. 目次 1. Rangeアダプタ 2. Rangeアダプタの性質 3. Rangeアダプタオブジェクト 4. Rangeアダプタクロージャオブジェクトとなる要件 5.

    Rangeアダプタクロージャオブジェクトを作る 6. Rangeアダプタオブジェクトを作る 7. ジェネレーターでviewを作る 1
  2. Rangeアダプタ 2 C++20でRangeアダプタが導入され、様々なRangeを簡単に加工できるようになった。 using namespace std; vector v {1, 2,

    3, 4, 5}; // reverse: 逆順にする println("{}", v | views::reverse) // [5, 4, 3, 2, 1] // take(n): 先頭からn個とる println("{}", v | views::reverse | views::take(3)) // [5, 4, 3] これを作りたい C++20ではRangeアダプタの性質をすべて満たす汎用的な実装は不可能 ⚫ 何がRangeアダプタとなるのかが規定されていない ⚫ 特定の処理系における実装は、標準ライブラリを真似すればできる C++23では可能
  3. Rangeアダプタの性質 3 Rangeアダプタクロージャオブジェクト(RACO): ⚫ rangeを受け取る単項関数オブジェクト (C++23) ⚫ パイプライン記法で、 | 演算子の右側に現れるオブジェクト

    ⚫ r をrange, raco をRangeアダプタクロージャオブジェクトとするとき、 r | raco と raco® は等しい Rangeアダプタオブジェクト(RAO): ⚫ 第1引数に viewable_range をとり view を返すカスタマイゼーションポイントオブ ジェクト ⚫ range 以外の引数をすべて渡すと、Rangeアダプタクロージャオブジェクトを返す ⚫ r をrange, rao をRangeアダプタオブジェクトとするとき、 r | rao(…) と rao(r, …) と rao(…)(r) は等しい
  4. Rangeアダプタオブジェクト 4 using namespace std; vector v {1, 2, 3,

    4, 5}; // 追加の引数を受け取る前のtakeはRAO、take(3)の値はRACO v | views::take(3); // RACO take(3) に対して関数記法でvを渡す views::take(3)(v); // RAO take に対してすべての引数を渡す views::take(v, 3); // reverse は追加の引数がなく、初めからRACO v | views::reverse; views::reverse(v);
  5. Rangeアダプタクロージャオブジェクトとなる要件 5 C++23では、型 T のオブジェクト t がRACOとなる要件が定められた。 ⚫ t はRangeを受け取る単項関数オブジェクトである

    ⚫ T は std::ranges::range_adaptor_closure<T> の派生クラスである ⚫ T 以外の型 U に対して、 T は std::ranges::range_adaptor_closure<U> の 派生クラスではない ⚫ T は range のモデルではない 以上の要件を満たせば、自作したクラスのオブジェクトでもRACOになる。 range_adaptor_closure 自体は空のクラス: namespace std::ranges { template<class D> requires is_class_v<D> && same_as<D, remove_cv_t<D>> class range_adaptor_closure { }; }
  6. Rangeアダプタクロージャオブジェクトを作る 6 RACOの実装例: class my_adaptor_closure : public std::ranges::range_adaptor_closure<my_adaptor_closure> { public:

    template <std::ranges::viewable_range R> constexpr auto operator()(R&& r) const { return my_adaptor_view(r); // rをラップしたviewを作って返す処理 } }; constexpr my_adaptor_closure my_adaptor; // 使用例 // v | my_adaptor my_adptor_viewクラスはあらかじめ定義しておく
  7. Rangeアダプタクロージャオブジェクトを作る 7 Rangeを受け取る単項関数オブジェクトをRACOに変換する例(P2387R3): template <typename F> class closure_t : public

    std::ranges::range_adaptor_closure<closure_t<F>> { F f; public: constexpr closure_t(F f) : f(f) { } template <std::ranges::viewable_range R> constexpr auto operator()(R&& r) const { return f(std::forward<R>(r)); } }; このようなものを用意すれば、ラムダ式で簡単にRACOを定義できる。
  8. Rangeアダプタクロージャオブジェクトを作る 8 例: 標準の join_view を作る別のRACOを定義する (viewまで作るのは大変なので) inline constexpr closure_t

    user_defined_join = []<std::ranges::viewable_range R> (R&& r) { return std::ranges::join_view(std::forward<R>(r)); }; // 使用例 // v | user_defined_join;
  9. Rangeアダプタオブジェクトを作る 9 追加の引数があるビューの場合、それらを受け取ってRACOを構築するためには Rangeアダプタオブジェクト(RAO) を定義する必要がある。 関数オブジェクトをラップしてRAOを定義する例 (P2387R3, closure_t は前述の定義): template

    <typename F> class adaptor_t { F f; public: constexpr adaptor_t(F f) : f(f) { } template <typename... Args> constexpr auto operator()(Args&&... args) const { if constexpr (std::invocable<F const&, Args...>) { // すべての引数が渡されたら、持っている関数を呼ぶ return f(std::forward<Args>(args)...); } else { // range 以外の引数が渡されたら、後ろから部分適用してRACOを作る return closure_t(std::bind_back(f, std::forward<Args>(args)...)); } } };
  10. Rangeアダプタオブジェクトを作る 10 例: 標準の take_view を作る別のRAOを定義する inline constexpr adaptor_t user_defined_take

    = []<std::ranges::viewable_range R> (R&& r, std::range_difference_t<R> d) { return std::ranges::take_view(std::forward<R>(r), d); }; // 使用例 // v | user_defined_take(3);
  11. ジェネレーターでviewを作る 11 std::generator は view であるため、ジェネレーターを使えば簡単に実装できる。 inline constexpr closure_t my_all

    = []<std::ranges::viewable_range R> (R&& r) -> std::generator<std::ranges::range_value_t<R>> { // 受け取ったRangeの要素をそのまま出力する for(auto&& i : r1) co_yield i; }; // 使用例 // v | my_all; ここでは、ラムダ式でジェネレーターを定義し、 前述のclosure_tでラップしてRACOにしている。 ジェネレーターはinput_rangeにしかならないので、Rangeの性質としてはそれほど良く ない。元のRangeの性質を温存するにはviewクラスの実装を頑張るしかない。
  12. ジェネレーターでviewを作る 12 concat の実装例 inline constexpr adaptor_t concat = []<std::ranges::viewable_range

    R1, std::ranges::viewable_range R2> requires std::same_as< std::ranges::range_value_t<R1>, std::ranges::range_value_t<R2>> (R1&& r1, R2&& r2) -> std::generator<std::ranges::range_value_t<R1>> { // 受け取った2つのRangeの要素を出力する for(auto&& i : r1) co_yield i; for(auto&& i : r2) co_yield i; }; // 使用例 // v1 | concat(v2);