LoginSignup
12
8

C++23のジェネレーターでレンジアダプタを自作する

Last updated at Posted at 2024-03-11

C++20でレンジライブラリが導入され、C++23ではレンジアダプタを自作するためのサポートが入りました。

しかし、フルスペックのレンジアダプタを作ろうと思うと、以下のものを用意する必要があり結構大変です。

  • ビュークラス
    元となるレンジを保持し、適切にイテレーターを作る
  • ビュークラスのイテレータークラス
    レンジアダプタのレンジに対する作用は遅延評価できる必要があるので、実際にはイテレーターがレンジアダプタの実際の仕事を行う
  • レンジアダプタクロージャオブジェクト
    パイプライン記法と関数記法をサポートしビュークラスのオブジェクトを作る
  • レンジアダプタオブジェクト
    レンジアダプタの引数の部分適用をサポートする

ここではstd::generatorを用いて簡単にビュークラスを作る方法を紹介します。

ジェネレーター

ジェネレーターを使うと、コルーチンが生成する値をviewとして提供できます。

#include <generator>

// 偶数値列を無限生成するコルーチン
std::generator<int> evens() {
  int n = 0;
  while (true) {
    co_yield n;
    n += 2;
  }
}

明示的なクラス定義は書いていませんが、std::generatorviewとなるので、これだけでビュークラスとそのイテレーターを作ることができます。

レンジアダプタクロージャオブジェクト

レンジアダプタクロージャオブジェクトは、レンジを受け取る単項関数オブジェクトです。

rcをレンジアダプタクロージャオブジェクト、range をレンジとするとき、以下の2つの式は等しくなります。

range | rc // パイプライン記法
rc(range)  // 関数記法

レンジライブラリの最大の特徴であるパイプライン記法はレンジアダプタクロージャオブジェクトによってサポートされています。

しかし、C++20の時点では、どうすればレンジアダプタクロージャオブジェクトを作れるのかという要件が規定されていなかったため、正式なレンジアダプタクロージャオブジェクトを自作するポータブルな方法がありませんでした。

C++23では std::ranges::range_adaptor_closure<T>の派生クラスTのオブジェクトは(その他の細かい要件を満たせば)レンジアダプタクロージャオブジェクトとなることが規定されたため、自作できるようになっています。

#include <ranges>

class closure_t : public std::ranges::range_adaptor_closure<closure_t> {
public:
  template <std::ranges::viewable_range R>
  constexpr auto operator()(R&& r) const {
    // このように初期化できるmy_viewクラスがあらかじめ定義してあるとして…
    return my_view(std::forward<R>(r));
  }
};

inline constexpr closure_t my_raco;

closure_t はレンジを受け取ってビューを作って返す operator() を実装することで関数記法をサポートしています。パイプライン記法のサポートは処理系によって提供されます。

レンジアダプタオブジェクト

レンジアダプタオブジェクトは、レンジアダプタの引数の部分適用をサポートするオブジェクトです。ビューを構築するのに元となるレンジ以外の引数が必要なとき、用意する必要があります。

raをレンジアダプタオブジェクト、range をレンジとするとき、以下の3つの式は等しくなります。

ra(range, args...)
range | ra(args...) 
ra(args...)(range)

ここで、式 ra(args...) の値がレンジアダプタクロージャオブジェクトとなります。

レンジアダプタを自作する

P2387R3 Pipe support for user-defined range adaptors に載っている、関数オブジェクトをレンジアダプタとしてラップする例を紹介します。

#include <ranges>
#include <functional>

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>
    requires std::invocable<F const&, R>
  constexpr auto operator()(R&& r) const {
    return f(std::forward<R>(r));
  }
};

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 {
      // bind_backで引数を後ろから部分適用する
      // 戻り値はrange_adaptor_closureの派生クラスではないのでclosure_tでラップする
      return closure_t(std::bind_back(f, std::forward<Args>(args)...));
    }
  }
};

このようにすると実際にビューを構築する関数さえ用意すればレンジアダプタを作ることができます。

// あまり意味はないが標準のtransform_viewを作るレンジアダプタを定義する
inline constexpr adaptor_t user_defined_transform
  = []<std::ranges::viewable_range R, typename F>
    (R&& r, F&& f) {
      return std::ranges::transform_view(std::forward<R>(r), std::forward<F>(f));
    };

本記事の趣旨に従い、ジェネレーターを使ってビュークラスを簡単に定義してみましょう。

ここでは、2つのレンジを連結するレンジアダプタ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つのレンジの要素を出力する
      for(auto&& i : r1) co_yield i;
      for(auto&& i : r2) co_yield i;
    };

早速試してみましょう。

int main() {
  std::vector v1 = {1, 2, 3};
  std::vector v2 = {10, 20, 30};

  std::println("{}", v1 | concat(v2));
}

この出力は次のようになり、レンジが連結されていることがわかります。

[1, 2, 3, 10, 20, 30]

まとめ

C++23ではジェネレーターを使うことでレンジアダプタを簡単に作れます。

なお、ジェネレーターは input_range にしかなりません。コルーチンの実行は進めることしかできず、戻ったり最初からやり直したりはできないためです。

ランダムアクセスしたいなど、もっと良い特性のビューを作りたい場合は従来通りビュークラスを作る必要があります。

12
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
8