[C++] rangesのパイプにアダプトするには

C++20の<ranges>のパイプ(|)に自作のview(Rangeアダプタ)を接続できるようにするにはどうすればいいのでしょうか?その方法は一見よくわからず、特に提供されてもいません。それでもできないことはないので、なんとかする話です。

パイプの実態

rangesのパイプは言語組み込みの機能ではなく、ビット論理和演算子|)をオーバーロードしたものです。そのため、単純には|オーバーロードを自作のviewに対して提供すれば良さそうに思えます。

しかし、よくあるパイプライン記法による記述を見てみると、それではダメそうなことがわかります。

int main() {
  using namespace std::views;

  auto seq = iota(1) | drop(5)
                     | filter([](int n) { return n % 2 == 0;})
                     | transform([](int n) { return n * 2; })
                     | take(5);
}

このチェーンの起点となっているのはiota(1)であり、これは入力となるrangeを生成しています。このiotaiota_viewというviewを返していて、このように引数から何かviewを生成しているものをRangeファクトリと呼びます。Rangeファクトリはこのiotaのようにパイプの最初で使用して入力となるrangeを生成するタイプのものです。今回どうにかしたいのはこれではありません。

iota(1)の後ろで、|で接続されているの(drop, filterなど)がRangeアダプタと呼ばれるもので、これはviewを入力として何かしらの変換を適用したviewを返すもので、これは必ず|の右辺に来ます。今回どうにかしたいのはこれであり、これはview型とは別のもので、どうやら型ではなさそうです。

ここで注意すべきなのは、Rangeファクトリの戻り値型は常にviewであるのに対して、Rangeアダプタの戻り値型はそうではないことです。例えばdrop(5)の戻り値型は引数に与えられた5を保持した何かを返しています。その後、|によってrangeを入力することでようやくviewを生成します(例えば、iota(1) | drop(5)の結果はdorp_viewになる)。

RangeファクトリをRF、RangeアダプタをRA、与える0個以上の引数をArgsとして、コンセプトっぽい書き方で表すと次のようになっています

  • Rangeファクトリ : RF(Args) -> view
  • Rangeアダプタ : RA(Args) -> ??
  • パイプライン : view | RA(Args) -> view

viewviewコンセプトを満たす型であることを表します。

この性質から分かるように、パイプライン演算子|)を提供しているのはRangeアダプタの戻り値型(上記の??)です。そして、自作のRangeアダプタをパイプにチェーンしたければこれらと同じことをする必要があります。

Rangeアダプタオブジェクト/Rangeアダプタクロージャオブジェクト

Rangeアダプタは関数のように見えますがそうではなく、カスタマイゼーションポイントオブジェクト(CPO)と呼ばれる関数オブジェクトの一種です。そのため、Rangeアダプタの実体のことをRangeアダプタオブジェクトと呼びます。

Rangeアダプタオブジェクトとは、1つ目の引数にviewable_rangeを受けて呼出可能なCPOでありその戻り値型はviewとなる、みたいに規定されています。その中でも、1引数のRangeアダプタオブジェクトのことを特に、Rangeアダプタクロージャオブジェクトと呼びます。

このRangeアダプタクロージャオブジェクトには規格によって変な性質が付加されています。

RangeアダプタクロージャオブジェクトCと入力のrange(正確には、viewable_range)オブジェクトrがあった時、次の2つの記述は同じ意味と効果を持ちます

C(r);   // 関数記法
r | C:  // パイプライン記法

ようはRangeアダプタクロージャオブジェクトに入力rangeを関数呼出とパイプラインの2つの方法で入力できるということです。先ほど見たように、この戻り値型はviewとなります(でなければなりません)。コンセプトを用いて書いてみると次のようになります

// 入力のrange(viewable_range)オブジェクト
viewable_range auto r = ...;

// この2つの呼び出しは同じviewを返す
view auto v1 = C(r);
view auto v2 = r | C ;

さらに、別のRangeアダプタクロージャオブジェクトDに対して、C | Dが有効である必要があり、その戻り値型はまたRangeアダプタクロージャオブジェクトである必要があります。

auto E = C | D;  // EはRangeアダプタクロージャオブジェクト

// これらの呼び出しは同じviewを返す
view auto v1 = r | C | D;
view auto v2 = r | (C | D) ;
view auto v3 = r | E ;
view auto v4 = E(r) ;

つまりは、Rangeアダプタクロージャオブジェクト同士もまた|で(事前に)接続可能であり、|は右結合となるということです。そしてその結果もRangeアダプタクロージャオブジェクトとなり、入力に対して順番に接続した時と同じ振る舞いをしなければなりません。ただし、Rangeアダプタクロージャオブジェクト同士の事前結合においては関数記法は求められていません。

auto E = D(C);  // これはできる必要はない(できない)

Rangeアダプタクロージャオブジェクトは1引数ですが、Rangeアダプタオブジェクトの中には追加の引数を受け取る者もいます(というかそっちの方が多い)。その場合、引数を渡してからrangeを入力しても、rangeと一緒に引数を渡しても、ほぼ同等な振る舞いをします。

view auto v1 = r | C(args...);
view auto v2 = C(r, args...);
view auto v3 = C(args...)(r);

つまりは、Rangeアダプタオブジェクトにその追加の引数args...をあらかじめ渡すことができて、その結果(C(args...))はRangeアダプタクロージャオブジェクトとなります。

ここまでくると、Rangeアダプタクロージャオブジェクトとは、このように追加の引数を全て部分適用して、あとは入力のrangeを受け取るだけになったRangeアダプタオブジェクト(1引数で呼出可能なRangeアダプタオブジェクト)、であることがわかります。そして、パイプで使用可能なRangeアダプタオブジェクトとはRangeアダプタクロージャオブジェクトのことです。

なお、事前結合が可能なのはRangeアダプタクロージャオブジェクトだけなので、そうではないRangeアダプタオブジェクトを事前に|で接続することはできません。

実例

int main() {
  auto seq = iota(1) | std::views::take(5);
}

ここでは、std::views::takeはRangeアダプタオブジェクトですがまだRangeアダプタクロージャオブジェクトではありません。take(5)によって必要な引数が満たされ、Rangeアダプタクロージャオブジェクトとなり、これで|で使用可能となります。そして、iota(1) | take(5)の結果はviewを生成します。

標準にあるRangeアダプタクロージャオブジェクトには例えばviews::commonがあります。

int main() {
  auto seq = iota(1) | std::views::common;
}

views::commonはすでにRangeアダプタクロージャオブジェクトであるので追加の引数を渡す必要がなく、そのままパイプで接続可能です。iota(1) | commonの結果はviewを生成します。

Rangeアダプタの事前適用は次のようになります

int main() {
  using namespace std::views;

  auto adoptor = drop(5)
               | filter([](int n) { return n % 2 == 0;})
               | transform([](int n) { return n * 2; })
               | take(5);

  auto seq = iota(1) | adoptor;
}

drop, filter, transform, take, adoptorは全てRangeアダプタオブジェクトであり、引数を与えて呼び出すことでRangeアダプタクロージャオブジェクトを生成しています。それらを|で接続して生成されたadopterもまたRangeアダプタクロージャオブジェクトであり、まだrangeは入力されていません。そして、iota(1) | adoptorviewを生成し、冒頭の全部まとめているコードと同じ振る舞いをします(ただし、ここではまだ処理を開始していないので何も始まっていません)。

自作のRangeアダプタ(view)でパイプを使用可能にするとは、そのviewのためのRangeアダプタオブジェクトを定義した上で、それそのものあるいはその呼出がRangeアダプタクロージャオブジェクトを返すようにし、そのRangeアダプタクロージャオブジェクト型に対して|オーバーロードし、なおかつ上記のRangeアダプタ(クロージャ)オブジェクトの性質を満たすようにしなければなりません。

標準ライブラリ実装による実装

やるべきことはわかりましたたが、そこはかとなく面倒臭そうですしどのように実装すれば適切なのかもよくわかりません。そこで、主要なC++標準ライブラリ実装がRangeアダプタをどのように実装しているのかを見てみます。

GCC 10

例えば、filter_viewview型)とviews::filter(Rangeアダプタオブジェクト)を見てみると、次のように定義されています

namespace std::ranges {

  ...

  template<input_range _Vp,
           indirect_unary_predicate<iterator_t<_Vp>> _Pred>
    requires view<_Vp> && is_object_v<_Pred>
  class filter_view : public view_interface<filter_view<_Vp, _Pred>>
  {
    ...
  };

  ...

  namespace views
  {
    inline constexpr __adaptor::_RangeAdaptor filter
      = [] <viewable_range _Range, typename _Pred> (_Range&& __r, _Pred&& __p)
      {
        return filter_view{std::forward<_Range>(__r), std::forward<_Pred>(__p)};
      };
  } // namespace views

}

また、Rangeアダプタクロージャオブジェクトviews::commoncommon_viewは次のように定義されています。

namespace std::ranges {

  ...

  template<view _Vp>
    requires (!common_range<_Vp>) && copyable<iterator_t<_Vp>>
  class common_view : public view_interface<common_view<_Vp>>
  {
    ...
  };

  ...

  namespace views
  {
    inline constexpr __adaptor::_RangeAdaptorClosure common
      = [] <viewable_range _Range> (_Range&& __r)
      {
        if constexpr (common_range<_Range>
                  && requires { views::all(std::forward<_Range>(__r)); })
          return views::all(std::forward<_Range>(__r));
        else
          return common_view{std::forward<_Range>(__r)};
      };

  } // namespace views
}

Rangeアダプタの実体型は__adaptor::_RangeAdaptor、Rangeアダプタクロージャオブジェクトの実体型は__adaptor::_RangeAdaptorClosureであるようです。省略しますが、他のRangeアダプタに対してもこれらと同様の実装方針が採られています。

まずはRangeアダプタの実装を見てみます。

template<typename _Callable>
struct _RangeAdaptor
{
protected:
  [[no_unique_address]]
   __detail::__maybe_present_t<!is_default_constructible_v<_Callable>, 
                               _Callable> _M_callable;

public:

  constexpr
  _RangeAdaptor(const _Callable& = {})
   requires is_default_constructible_v<_Callable>
  { }

  constexpr
  _RangeAdaptor(_Callable __callable)
   requires (!is_default_constructible_v<_Callable>)
   : _M_callable(std::move(__callable))
  { }

  template<typename... _Args>
    requires (sizeof...(_Args) >= 1)
    constexpr auto
    operator()(_Args&&... __args) const
    {
     // [range.adaptor.object]: If a range adaptor object accepts more
     // than one argument, then the following expressions are equivalent:
     //
     //   (1) adaptor(range, args...)
     //   (2) adaptor(args...)(range)
     //   (3) range | adaptor(args...)
     //
     // In this case, adaptor(args...) is a range adaptor closure object.
     //
     // We handle (1) and (2) here, and (3) is just a special case of a
     // more general case already handled by _RangeAdaptorClosure.
     if constexpr (is_invocable_v<_Callable, _Args...>)
       {
          static_assert(sizeof...(_Args) != 1,
                  "a _RangeAdaptor that accepts only one argument "
                  "should be defined as a _RangeAdaptorClosure");
          // Here we handle adaptor(range, args...) -- just forward all
          // arguments to the underlying adaptor routine.
          return _Callable{}(std::forward<_Args>(__args)...);
       }
     else
       {
          // Here we handle adaptor(args...)(range).
          // Given args..., we return a _RangeAdaptorClosure that takes a
          // range argument, such that (2) is equivalent to (1).
          //
          // We need to be careful about how we capture args... in this
          // closure.  By using __maybe_refwrap, we capture lvalue
          // references by reference (through a reference_wrapper) and
          // otherwise capture by value.
          auto __closure
            = [...__args(__maybe_refwrap(std::forward<_Args>(__args)))]
              <typename _Range> (_Range&& __r) {
                // This static_cast has two purposes: it forwards a
                // reference_wrapper<T> capture as a T&, and otherwise
                // forwards the captured argument as an rvalue.
                return _Callable{}(std::forward<_Range>(__r),
                          (static_cast<unwrap_reference_t
                                           <remove_const_t<decltype(__args)>>>
                            (__args))...);
              };
          using _ClosureType = decltype(__closure);
          return _RangeAdaptorClosure<_ClosureType>(std::move(__closure));
       }
   }
};

template<typename _Callable>
  _RangeAdaptor(_Callable) -> _RangeAdaptor<_Callable>;

めちゃくちゃ複雑なので細かく解説はしませんが、ここではRangeアダプタオブジェクト(not クロージャオブジェクト)の追加の引数を事前に受け取って保持しておくことができる、という性質を実装しています。

view auto v1 = r | C(args...);  // #1
view auto v2 = C(r, args...);   // #2
view auto v3 = C(args...)(r);   // #3

このクラスは何か呼出可能と思われるもの(_Callable)を受け取って、それがデフォルト構築不可能な場合のみメンバ(_M_callable)に保存しています。最初に見た使われ方では、ラムダ式によって初期化されていて、そのラムダ式で対象のviewに合わせたRangeアダプタの処理が実装されていました。

Rangeアダプタの性質を実装しているのはoperator()内で、ここでは上記#2, #3の2つのケースを処理していて、#1はRangeアダプタクロージャオブジェクト(_RangeAdaptorClosure)のパイプライン演算子に委ねています。

operator()内、constexpr iftrue分岐では、C(r, args...)を処理しています。この場合は引数列__argsの1つ目に入力rangeを含んでおり、残りの引数を保存する必要もないため、それらをそのまま転送して_Callableを呼び出し、それによってRangeアダプタを実行します。この場合の戻り値はviewとなります。

constexpr iffalse分岐では、C(args...)(r)を処理しています。この場合は引数列__argsに入力rangeは含まれておらず、それは後から入力(|or())されるので、渡された引数列を保存して後から入力rangeと共に_Callableの遅延呼び出しを行う呼び出し可能ラッパを返しています。それはラムダ式で実装されており、引数の保存はキャプチャによって行われています。この場合の戻り値はRangeアダプタクロージャオブジェクトであり、引数と_Callableを内包したラムダ式_RangeAdaptorClosureに包んで返しています。

どちらの場合でもメンバに保存した_M_callableを使用していませんが、この#1, #2のケースの場合はどちらも_Callableがデフォルト構築可能であることを仮定することができます。なぜなら、この二つの場合にわたってくる_Callableは状態を持たないラムダ式であり、C++20からそれはデフォルト構築可能であり、それはRangeアダプタオブジェクト定義時に渡されるものだからです。_M_callableを使用する必要があるのは実はC(args...)相当の部分適用をおこなった場合のみで、それはRangeアダプタクロージャオブジェクト(_RangeAdaptorClosure)において処理されます。

次はそのRangeアダプタクロージャオブジェクトの実装を見てみましょう。

template<typename _Callable>
struct _RangeAdaptorClosure : public _RangeAdaptor<_Callable>
{
  using _RangeAdaptor<_Callable>::_RangeAdaptor;

  template<viewable_range _Range>
    requires requires { declval<_Callable>()(declval<_Range>()); }
  constexpr auto
  operator()(_Range&& __r) const
  {
    if constexpr (is_default_constructible_v<_Callable>)
      return _Callable{}(std::forward<_Range>(__r));
    else
      return this->_M_callable(std::forward<_Range>(__r));
  }

  // 1. range | RACO -> view
  template<viewable_range _Range>
    requires requires { declval<_Callable>()(declval<_Range>()); }
  friend constexpr auto
  operator|(_Range&& __r, const _RangeAdaptorClosure& __o)
  { return __o(std::forward<_Range>(__r)); }

  // 2. RACO | RACO -> RACO
  template<typename _Tp>
  friend constexpr auto
  operator|(const _RangeAdaptorClosure<_Tp>& __x,
            const _RangeAdaptorClosure& __y)
  {
    if constexpr (is_default_constructible_v<_Tp>
                  && is_default_constructible_v<_Callable>)
      {
        auto __closure = [] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | decltype(__x){} | decltype(__y){};
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
    else if constexpr (is_default_constructible_v<_Tp>
                       && !is_default_constructible_v<_Callable>)
      {
        auto __closure = [__y] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | decltype(__x){} | __y;
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
    else if constexpr (!is_default_constructible_v<_Tp>
                       && is_default_constructible_v<_Callable>)
      {
        auto __closure = [__x] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | __x | decltype(__y){};
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
    else
      {
        auto __closure = [__x, __y] <typename _Up> (_Up&& __e) {
          return std::forward<_Up>(__e) | __x | __y;
        };
        return _RangeAdaptorClosure<decltype(__closure)>(__closure);
      }
  }
};

template<typename _Callable>
  _RangeAdaptorClosure(_Callable) -> _RangeAdaptorClosure<_Callable>;

まず見て分かるように、_RangeAdaptorClosure_RangeAdaptorを継承していて、受けた呼出可能なものの保持などは先ほどの_RangeAdaptorと共通です。そして、2つのoperator|オーバーロードが定義されています。この実装方法はHidden friendsと呼ばれる実装になっています。

Rangeアダプタクロージャオブジェクトは1つのrangeを関数呼出によって入力することができ、それはoperator()で実装されています。ここで、_Callableがデフォルト構築可能かによって_RangeAdaptor::_M_callableを使用するかの切り替えが初めて行われており、_Callableがデフォルト構築可能ではない場合というのは、Rangeアダプタに追加の引数を部分適用した結果生成されたRangeアダプタクロージャオブジェクトの場合のみで、それは_RangeAdaptor::operator()constexpr iffalseパートの結果として生成されます。

1つ目のoperator|オーバーロードは追記コメントにあるように、左辺にrangeを受けて結合する場合の|オーバーロードです(range | RACO -> view)。この場合は先ほどの関数呼び出しと同じことになるので、operator()に委譲されています。わかりづらいですが、2つ目の引数の__o*thisに対応しています。

2つ目のoperator|オーバーロードは残った振る舞い、すなわちRangeアダプタクロージャオブジェクト同士の事前結合を担っています。なんかifで4分岐しているのは、引数の_RangeAdaptorClosureオブジェクトの_Callableがデフォルト構築可能か否かでメンバの_M_callableを参照するかが変化するためで、それが引数2つ分の2x2で4パターンの分岐になっています。実際の結合処理は1つ目の|に委譲していて、その処理はラムダ式で記述して、そのラムダ式のオブジェクトを_RangeAdaptorClosureに包んで返すことで戻り値は再びRangeアダプタクロージャオブジェクトになります。ifの分岐の差異は必要な場合にのみ引数__x, __yを返すラムダにキャプチャしていることです。

GCC10の実装では、Rangeアダプタとしての動作はステートレスなラムダ式で与えられ、Rangeアダプタオブジェクトはそれを受けたこの2つの型のどちらかのオブジェクトとなり、ややこしい性質の実装はこの2つの型に集約され共通化されています。<ranges>のパイプライン演算子_RangeAdaptorClosureに定義されたものが常に使用されています。

GCC 11

GCC11になると、この実装が少し変化していました。

namespace std::ranges {

  // views::filter
  namespace views
  {
    namespace __detail
    {
      template<typename _Range, typename _Pred>
          concept __can_filter_view
            = requires { filter_view(std::declval<_Range>(), std::declval<_Pred>()); };
    } // namespace __detail

    struct _Filter : __adaptor::_RangeAdaptor<_Filter>
    {
      template<viewable_range _Range, typename _Pred>
        requires __detail::__can_filter_view<_Range, _Pred>
        constexpr auto
        operator()(_Range&& __r, _Pred&& __p) const
        {
          return filter_view(std::forward<_Range>(__r), std::forward<_Pred>(__p));
        }

      using _RangeAdaptor<_Filter>::operator();
      static constexpr int _S_arity = 2;
      static constexpr bool _S_has_simple_extra_args = true;
    };

    inline constexpr _Filter filter;
  } // namespace views

  // views::common
  namespace views
  {
    namespace __detail
    {
      template<typename _Range>
        concept __already_common = common_range<_Range>
          && requires { views::all(std::declval<_Range>()); };

      template<typename _Range>
        concept __can_common_view
          = requires { common_view{std::declval<_Range>()}; };
    } // namespace __detail

    struct _Common : __adaptor::_RangeAdaptorClosure
    {
      template<viewable_range _Range>
        requires __detail::__already_common<_Range>
          || __detail::__can_common_view<_Range>
        constexpr auto
        operator()(_Range&& __r) const
        {
          if constexpr (__detail::__already_common<_Range>)
            return views::all(std::forward<_Range>(__r));
          else
            return common_view{std::forward<_Range>(__r)};
        }

      static constexpr bool _S_has_simple_call_op = true;
    };

    inline constexpr _Common common;
  } // namespace views
}

filtercommonだけを見ても、_RangeAdaptorClosureとかの名前そのものは変わっていなくてもその使い方が大きく変わっていることがわかります。どちらも継承して使用されていて、_RangeAdaptorはCRTPになっています。それらに目を向けてみると

// The base class of every range adaptor non-closure.
//
// The static data member _Derived::_S_arity must contain the total number of
// arguments that the adaptor takes, and the class _Derived must introduce
// _RangeAdaptor::operator() into the class scope via a using-declaration.
//
// The optional static data member _Derived::_S_has_simple_extra_args should
// be defined to true if the behavior of this adaptor is independent of the
// constness/value category of the extra arguments.  This data member could
// also be defined as a variable template parameterized by the types of the
// extra arguments.
template<typename _Derived>
struct _RangeAdaptor
{
  // Partially apply the arguments __args to the range adaptor _Derived,
  // returning a range adaptor closure object.
  template<typename... _Args>
    requires __adaptor_partial_app_viable<_Derived, _Args...>
    constexpr auto
    operator()(_Args&&... __args) const
    {
      return _Partial<_Derived, decay_t<_Args>...>{std::forward<_Args>(__args)...};
    }
};
// The base class of every range adaptor closure.
//
// The derived class should define the optional static data member
// _S_has_simple_call_op to true if the behavior of this adaptor is
// independent of the constness/value category of the adaptor object.
struct _RangeAdaptorClosure
{

  // 1. range | RACO -> view  ※説明のため追記
  // range | adaptor is equivalent to adaptor(range).
  template<typename _Self, typename _Range>
    requires derived_from<remove_cvref_t<_Self>, _RangeAdaptorClosure>
      && __adaptor_invocable<_Self, _Range>
    friend constexpr auto
    operator|(_Range&& __r, _Self&& __self)
    { return std::forward<_Self>(__self)(std::forward<_Range>(__r)); }

  // 2. RACO | RACO -> RACO ※説明のため追記
  // Compose the adaptors __lhs and __rhs into a pipeline, returning
  // another range adaptor closure object.
  template<typename _Lhs, typename _Rhs>
    requires derived_from<_Lhs, _RangeAdaptorClosure>
      && derived_from<_Rhs, _RangeAdaptorClosure>
    friend constexpr auto
    operator|(_Lhs __lhs, _Rhs __rhs)
    { return _Pipe<_Lhs, _Rhs>{std::move(__lhs), std::move(__rhs)}; }
};

この二つのクラスの実装そのものはかなりシンプルになっています。_RangeAdaptor::operator()でRangeアダプタの性質(追加の引数を部分適用してRangeアダプタクロージャオブジェクトを生成する)を実装していて、_RangeAdaptorClosure::operator|でパイプライン演算子を実装しているのも先ほどと変わりありません。

ただし、どちらの場合もその実装詳細を_Partial_Pipeという二つの謎のクラスに委譲しています。これらのクラスの実装は複雑で長いので省略しますが、_PartialはRangeアダプタの追加の引数を保存してRangeアダプタクロージャオブジェクトとなる呼び出し可能なラッパ型で、_PipeはRangeアダプタクロージャオブジェクト2つを保持したRangeアダプタクロージャオブジェクトとなる呼び出し可能なラッパ型です。

_Partial_Pipeはどちらも部分特殊化を使用することで、渡された追加の引数/Rangeアダプタクロージャオブジェクトを効率的に保持しようとします。_RangeAdaptor/_RangeAdaptorClosureを継承するクラス型に_S_has_simple_call_opとか_S_has_simple_extra_argsだとかの静的メンバが生えているのは、これを適切に制御するためでもあります。

実装が細分化され分量が増えて利用方法も変化していますが、基本的にやっていることはGCC10の時と大きく変わってはいません。

これらの変更はおそらく、P2281の採択とP2287を意識したものだと思われます(どちらもC++23では採択済)。

MSVC

同じように、MSVCの実装も見てみます。view型とそのアダプタの関係性は変わらないので、以降はRangeアダプタだけに焦点を絞ります。

views::filter(Rangeアダプタオブジェクト)

namespace views {
    struct _Filter_fn {
        // clang-format off
        template <viewable_range _Rng, class _Pr>
        _NODISCARD constexpr auto operator()(_Rng&& _Range, _Pr&& _Pred) const noexcept(noexcept(
            filter_view(_STD forward<_Rng>(_Range), _STD forward<_Pr>(_Pred)))) requires requires {
            filter_view(static_cast<_Rng&&>(_Range), _STD forward<_Pr>(_Pred));
        } {
            // clang-format on
            return filter_view(_STD forward<_Rng>(_Range), _STD forward<_Pr>(_Pred));
        }

        // clang-format off
        template <class _Pr>
            requires constructible_from<decay_t<_Pr>, _Pr>
        _NODISCARD constexpr auto operator()(_Pr&& _Pred) const
            noexcept(is_nothrow_constructible_v<decay_t<_Pr>, _Pr>) {
            // clang-format on
            return _Range_closure<_Filter_fn, decay_t<_Pr>>{_STD forward<_Pr>(_Pred)};
        }
    };

    inline constexpr _Filter_fn filter;
} // namespace views

views::common(Rangeアダプタクロージャオブジェクト)

namespace views {
    class _Common_fn : public _Pipe::_Base<_Common_fn> {
    private:
        enum class _St { _None, _All, _Common };

        template <class _Rng>
        _NODISCARD static _CONSTEVAL _Choice_t<_St> _Choose() noexcept {
            if constexpr (common_range<_Rng>) {
                return {_St::_All, noexcept(views::all(_STD declval<_Rng>()))};
            } else if constexpr (copyable<iterator_t<_Rng>>) {
                return {_St::_Common, noexcept(common_view{_STD declval<_Rng>()})};
            } else {
                return {_St::_None};
            }
        }

        template <class _Rng>
        static constexpr _Choice_t<_St> _Choice = _Choose<_Rng>();

    public:
        // clang-format off
        template <viewable_range _Rng>
            requires (_Choice<_Rng>._Strategy != _St::_None)
        _NODISCARD constexpr auto operator()(_Rng&& _Range) const noexcept(_Choice<_Rng>._No_throw) {
            // clang-format on
            constexpr _St _Strat = _Choice<_Rng>._Strategy;

            if constexpr (_Strat == _St::_All) {
                return views::all(_STD forward<_Rng>(_Range));
            } else if constexpr (_Strat == _St::_Common) {
                return common_view{_STD forward<_Rng>(_Range)};
            } else {
                static_assert(_Always_false<_Rng>, "Should be unreachable");
            }
        }
    };

    inline constexpr _Common_fn common;
} // namespace views

雰囲気はGCC11の実装に似ています。Rangeアダプタオブジェクトでは、rangeを受け取る方の呼び出しをoperator()でその場(Rangeアダプタ型内部)で定義し外出し(共通化)しておらず、追加の引数を部分適用してRangeアダプタクロージャオブジェクトを返す呼出では_Range_closureという型に自身と追加の引数をラップして返しています。

Rangeアダプタクロージャオブジェクトでは、_Pipe::_Baseといういかにもな名前の型を継承しています。どうやら、パイプライン演算子はそこで定義されているようです。

まずはRangeアダプタの引数の部分適用時に返されるラッパ型_Range_closureを見てみます。

template <class _Fn, class... _Types>
class _Range_closure : public _Pipe::_Base<_Range_closure<_Fn, _Types...>> {
public:
    // We assume that _Fn is the type of a customization point object. That means
    // 1. The behavior of operator() is independent of cvref qualifiers, so we can use `invocable<_Fn, ` without
    //    loss of generality, and
    // 2. _Fn must be default-constructible and stateless, so we can create instances "on-the-fly" and avoid
    //    storing a copy.

    // Types(追加の引数)は参照やconstではないこと
    _STL_INTERNAL_STATIC_ASSERT((same_as<decay_t<_Types>, _Types> && ...));
    // _Fn(Rangeアダプタ型 not クロージャ型)はステートレスクラスかつデフォルト構築可能であること
    _STL_INTERNAL_STATIC_ASSERT(is_empty_v<_Fn>&& is_default_constructible_v<_Fn>);

    // clang-format off
    template <class... _UTypes>
        requires (same_as<decay_t<_UTypes>, _Types> && ...)
    constexpr explicit _Range_closure(_UTypes&&... _Args) noexcept(
        conjunction_v<is_nothrow_constructible<_Types, _UTypes>...>)
        : _Captures(_STD forward<_UTypes>(_Args)...) {}
    // clang-format on

    void operator()(auto&&) &       = delete;
    void operator()(auto&&) const&  = delete;
    void operator()(auto&&) &&      = delete;
    void operator()(auto&&) const&& = delete;

    using _Indices = index_sequence_for<_Types...>;

    template <class _Ty>
        requires invocable<_Fn, _Ty, _Types&...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) & noexcept(
        noexcept(_Call(*this, _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(*this, _STD forward<_Ty>(_Arg), _Indices{});
    }

    template <class _Ty>
        requires invocable<_Fn, _Ty, const _Types&...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) const& noexcept(
        noexcept(_Call(*this, _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(*this, _STD forward<_Ty>(_Arg), _Indices{});
    }

    template <class _Ty>
        requires invocable<_Fn, _Ty, _Types...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) && noexcept(
        noexcept(_Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{});
    }

    template <class _Ty>
        requires invocable<_Fn, _Ty, const _Types...>
    constexpr decltype(auto) operator()(_Ty&& _Arg) const&& noexcept(
        noexcept(_Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{}))) {
        return _Call(_STD move(*this), _STD forward<_Ty>(_Arg), _Indices{});
    }

private:
    template <class _SelfTy, class _Ty, size_t... _Idx>
    static constexpr decltype(auto) _Call(_SelfTy&& _Self, _Ty&& _Arg, index_sequence<_Idx...>) noexcept(
        noexcept(_Fn{}(_STD forward<_Ty>(_Arg), _STD get<_Idx>(_STD forward<_SelfTy>(_Self)._Captures)...))) {
        _STL_INTERNAL_STATIC_ASSERT(same_as<index_sequence<_Idx...>, _Indices>);
        return _Fn{}(_STD forward<_Ty>(_Arg), _STD get<_Idx>(_STD forward<_SelfTy>(_Self)._Captures)...);
    }

    tuple<_Types...> _Captures;
};

やたら複雑ですが、4つあるoperator()は値カテゴリの違いでムーブしたりしなかったりしているだけで、実質同じことをしています。テンプレートパラメータの_Fnviews::filterの実装で見たように、まだクロージャではないRangeアダプタ型です。追加の引数はTypes...で、静的アサートにも表れているように参照やconstを外すことでコピー/ムーブして(メンバのtupleオブジェクトに)保持されています。

このクラスはRangeアダプタオブジェクトとその追加の引数をラップしてRangeアダプタクロージャオブジェクトとなるものなので、operator()がやることは入力のrangeを受け取って、ラップしているRangeアダプタに同じくラップしている追加の引数とともに渡してviewを生成することです。その実態は_Call()関数であり、_Arg(入力rangeオブジェクト)->_Captures(追加の引数列)をこの順番で_Fn(Rangeアダプタ型)の関数呼び出し演算子に渡しています。_Fnは常にデフォルト構築可能であること強制することで追加のストレージを節約しており、_Fnの関数呼び出し演算子_Argと共に呼び出すとそこで直接定義されている入力rangeを受け取る処理が実行されます。例えばviews::filterの場合は1つ目のoperator()がそれにあたり、filter_viewの生成を行っています。

_Range_closureもまた、_Pipe::_Baseを継承することで|の実装を委譲しています。次はこれを見てみます。

namespace _Pipe {
  // clang-format off
  // C | R = C(R)の呼び出しが可能かを調べるコンセプト
  template <class _Left, class _Right>
  concept _Can_pipe = requires(_Left&& __l, _Right&& __r) {
      static_cast<_Right&&>(__r)(static_cast<_Left&&>(__l));
  };

  // Rangeアダプタクロージャオブジェクト同士の結合の要件をチェックするコンセプト
  // 共に、コピーorムーブできること
  template <class _Left, class _Right>
  concept _Can_compose = constructible_from<remove_cvref_t<_Left>, _Left>
      && constructible_from<remove_cvref_t<_Right>, _Right>;
  // clang-format on

  // 前方宣言
  template <class, class>
  struct _Pipeline;

  // Rangeアダプタクロージャオブジェクト型にパイプラインを提供する共通クラス
  template <class _Derived>
  struct _Base {
      template <class _Other>
          requires _Can_compose<_Derived, _Other>
      constexpr auto operator|(_Base<_Other>&& __r) && noexcept(
          noexcept(_Pipeline{static_cast<_Derived&&>(*this), static_cast<_Other&&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<_Derived&&>(*this), static_cast<_Other&&>(__r)};
      }

      template <class _Other>
          requires _Can_compose<_Derived, const _Other&>
      constexpr auto operator|(const _Base<_Other>& __r) && noexcept(
          noexcept(_Pipeline{static_cast<_Derived&&>(*this), static_cast<const _Other&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<_Derived&&>(*this), static_cast<const _Other&>(__r)};
      }

      template <class _Other>
          requires _Can_compose<const _Derived&, _Other>
      constexpr auto operator|(_Base<_Other>&& __r) const& noexcept(
          noexcept(_Pipeline{static_cast<const _Derived&>(*this), static_cast<_Other&&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<const _Derived&>(*this), static_cast<_Other&&>(__r)};
      }

      template <class _Other>
          requires _Can_compose<const _Derived&, const _Other&>
      constexpr auto operator|(const _Base<_Other>& __r) const& noexcept(
          noexcept(_Pipeline{static_cast<const _Derived&>(*this), static_cast<const _Other&>(__r)})) {
          // |両辺のCRTPチェック
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Derived, _Base<_Derived>>);
          _STL_INTERNAL_STATIC_ASSERT(derived_from<_Other, _Base<_Other>>);
          return _Pipeline{static_cast<const _Derived&>(*this), static_cast<const _Other&>(__r)};
      }

      template <_Can_pipe<const _Derived&> _Left>
      friend constexpr auto operator|(_Left&& __l, const _Base& __r)
#ifdef __EDG__ // TRANSITION, VSO-1222776
          noexcept(noexcept(_STD declval<const _Derived&>()(_STD forward<_Left>(__l))))
#else // ^^^ workaround / no workaround vvv
          noexcept(noexcept(static_cast<const _Derived&>(__r)(_STD forward<_Left>(__l))))
#endif // TRANSITION, VSO-1222776
      {
          return static_cast<const _Derived&>(__r)(_STD forward<_Left>(__l));
      }

      template <_Can_pipe<_Derived> _Left>
      friend constexpr auto operator|(_Left&& __l, _Base&& __r)
#ifdef __EDG__ // TRANSITION, VSO-1222776
          noexcept(noexcept(_STD declval<_Derived>()(_STD forward<_Left>(__l))))
#else // ^^^ workaround / no workaround vvv
          noexcept(noexcept(static_cast<_Derived&&>(__r)(_STD forward<_Left>(__l))))
#endif // TRANSITION, VSO-1222776
      {
          return static_cast<_Derived&&>(__r)(_STD forward<_Left>(__l));
      }
  };


  // Rangeアダプタクロージャオブジェクト同士の事前結合を担うラッパ型
  template <class _Left, class _Right>
  struct _Pipeline : _Base<_Pipeline<_Left, _Right>> {
      /* [[no_unique_address]] */ _Left __l;
      /* [[no_unique_address]] */ _Right __r;

      template <class _Ty1, class _Ty2>
      constexpr explicit _Pipeline(_Ty1&& _Val1, _Ty2&& _Val2) noexcept(
          is_nothrow_convertible_v<_Ty1, _Left>&& is_nothrow_convertible_v<_Ty2, _Right>)
          : __l(_STD forward<_Ty1>(_Val1)), __r(_STD forward<_Ty2>(_Val2)) {}

      template <class _Ty>
      _NODISCARD constexpr auto operator()(_Ty&& _Val) noexcept(
          noexcept(__r(__l(_STD forward<_Ty>(_Val))))) requires requires {
          __r(__l(static_cast<_Ty&&>(_Val)));
      }
      { return __r(__l(_STD forward<_Ty>(_Val))); }

      template <class _Ty>
      _NODISCARD constexpr auto operator()(_Ty&& _Val) const
          noexcept(noexcept(__r(__l(_STD forward<_Ty>(_Val))))) requires requires {
          __r(__l(static_cast<_Ty&&>(_Val)));
      }
      { return __r(__l(_STD forward<_Ty>(_Val))); }
  };

  template <class _Ty1, class _Ty2>
  _Pipeline(_Ty1, _Ty2) -> _Pipeline<_Ty1, _Ty2>;
} // namespace _Pipe

_Pipe::_Baseには2種類6つのoperator|が定義されています。_Can_composeコンセプトで制約されている最初の4つがRangeアダプタクロージャオブジェクト同士の事前結合を行うパイプ演算子で、4つあるのは値カテゴリの違いで*thisを適応的にムーブするためです。このクラスはCRTPで利用され、_Derived型は常にRangeアダプタクロージャオブジェクト型です。views::commonのように最初からRangeアダプタクロージャオブジェクトである場合は_Derivedはステートレスですが、views::filterのように追加の引数を受け取る場合は_Derivedは何かを保持しています。この結果は再びRangeアダプタクロージャオブジェクトとなるため、_Pipeline型がそのラッピングを担っています。_Pipeline_Pipe::_Baseを継承することで|の実装を省略しています。その関数呼び出し演算子ではrange | __l | __rの接続が__r(__l(range))となるように呼び出しを行っています。

残った2つがrange | RACO -> viewの形の接続(|によるrangeの入力)を行っているパイプ演算子で、この場合の_Left型の__lが入力のrangeオブジェクトです。__r*thisであり、パラメータを明示化していることで不要なキャストやチェックが省略できています(ここにはDeducing thisの有用性の一端を垣間見ることができます)。この場合は__l | __rの形の接続が__r(__L)の呼び出しと同等になる必要があり、そのような呼び出しを行っています。
なぜこっちだけHidden friendsになっているかというと、この場合はthisパラメータが|の右辺に来るように定義する必要があるため非メンバで定義せざるを得ないからです(メンバ定義だと常に左辺にthisパラメータが来てしまう)。

GCCがRangeアダプタオブジェクトのoperator()(引数を部分適用する方)の実装をも共通クラスに外出ししていたのに対して、MSVCはそうしていません。そのおかげだと思いますが、実装がだいぶシンプルに収まっています(値カテゴリの違いで必要になる4つのオーバーロードから目を逸らしつつ)。

どうやらMSVCは早い段階からこのような実装となっていたようで、P2281P2287の二つの変更はいずれもMSVCのこれらの実装をモデルケースとして標準に反映するものでした。

clang

views::filter

namespace views {
namespace __filter {
  struct __fn {
    template<class _Range, class _Pred>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Range&& __range, _Pred&& __pred) const
      noexcept(noexcept(filter_view(std::forward<_Range>(__range), std::forward<_Pred>(__pred))))
      -> decltype(      filter_view(std::forward<_Range>(__range), std::forward<_Pred>(__pred)))
      { return          filter_view(std::forward<_Range>(__range), std::forward<_Pred>(__pred)); }

    template<class _Pred>
      requires constructible_from<decay_t<_Pred>, _Pred>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Pred&& __pred) const
      noexcept(is_nothrow_constructible_v<decay_t<_Pred>, _Pred>)
    { return __range_adaptor_closure_t(std::__bind_back(*this, std::forward<_Pred>(__pred))); }
  };
} // namespace __filter

inline namespace __cpo {
  inline constexpr auto filter = __filter::__fn{};
} // namespace __cpo
} // namespace views

views::common

namespace views {
namespace __common {
  struct __fn : __range_adaptor_closure<__fn> {
    template<class _Range>
      requires common_range<_Range>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Range&& __range) const
      noexcept(noexcept(views::all(std::forward<_Range>(__range))))
      -> decltype(      views::all(std::forward<_Range>(__range)))
      { return          views::all(std::forward<_Range>(__range)); }

    template<class _Range>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    constexpr auto operator()(_Range&& __range) const
      noexcept(noexcept(common_view{std::forward<_Range>(__range)}))
      -> decltype(      common_view{std::forward<_Range>(__range)})
      { return          common_view{std::forward<_Range>(__range)}; }
  };
} // namespace __common

inline namespace __cpo {
  inline constexpr auto common = __common::__fn{};
} // namespace __cpo
} // namespace views

clangの実装はMSVCのものにかなり近いことが分かるでしょう。Rangeアダプタの共通実装は提供しておらず、Rangeアダプタクロージャオブジェクトの共通実装は__range_adaptor_closure_t__range_adaptor_closureというCRTP型を使用しています。

初期コミット時のメッセージによれば、P2287をベースとした実装であり、P2287はMSVCの実装を参考にしていたので、結果として似た実装となっているようです。

// CRTP base that one can derive from in order to be considered a range adaptor closure
// by the library. When deriving from this class, a pipe operator will be provided to
// make the following hold:
// - `x | f` is equivalent to `f(x)`
// - `f1 | f2` is an adaptor closure `g` such that `g(x)` is equivalent to `f2(f1(x))`
template <class _Tp>
struct __range_adaptor_closure;

// Type that wraps an arbitrary function object and makes it into a range adaptor closure,
// i.e. something that can be called via the `x | f` notation.
template <class _Fn>
struct __range_adaptor_closure_t : _Fn, __range_adaptor_closure<__range_adaptor_closure_t<_Fn>> {
    constexpr explicit __range_adaptor_closure_t(_Fn&& __f) : _Fn(std::move(__f)) { }
};

template <class _Tp>
concept _RangeAdaptorClosure = derived_from<remove_cvref_t<_Tp>, __range_adaptor_closure<remove_cvref_t<_Tp>>>;

template <class _Tp>
struct __range_adaptor_closure {
    template <ranges::viewable_range _View, _RangeAdaptorClosure _Closure>
        requires same_as<_Tp, remove_cvref_t<_Closure>> &&
                 invocable<_Closure, _View>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    friend constexpr decltype(auto) operator|(_View&& __view, _Closure&& __closure)
        noexcept(is_nothrow_invocable_v<_Closure, _View>)
    { return std::invoke(std::forward<_Closure>(__closure), std::forward<_View>(__view)); }

    template <_RangeAdaptorClosure _Closure, _RangeAdaptorClosure _OtherClosure>
        requires same_as<_Tp, remove_cvref_t<_Closure>> &&
                 constructible_from<decay_t<_Closure>, _Closure> &&
                 constructible_from<decay_t<_OtherClosure>, _OtherClosure>
    [[nodiscard]] _LIBCPP_HIDE_FROM_ABI
    friend constexpr auto operator|(_Closure&& __c1, _OtherClosure&& __c2)
        noexcept(is_nothrow_constructible_v<decay_t<_Closure>, _Closure> &&
                 is_nothrow_constructible_v<decay_t<_OtherClosure>, _OtherClosure>)
    { return __range_adaptor_closure_t(std::__compose(std::forward<_OtherClosure>(__c2), std::forward<_Closure>(__c1))); }
};

__range_adaptor_closure_tのテンプレートパラメータ_FnはRangeアダプタ型で、__range_adaptor_closure_t_Fn__range_adaptor_closureを基底に持ち、operator|__range_adaptor_closureで定義されています。

__range_adaptor_closureもまたCRTPで、operator|は2つともHidden friendsであり、1つ目がrangeを入力する方、2つ目がRangeアダプタクロージャオブジェクト同士の接続をする方、に対応しています。どちらでも、_Closure型の方がthisパラメータで_Tpと同じ型となることが制約されています。

Rangeアダプタ型(の部分適用operator())で使用される場合_Tpは一つ上の__range_adaptor_closure_tとなり、Rangeアダプタクロージャオブジェクト型で(継承して)使用される場合は_TpはそのRangeアダプタクロージャオブジェクト型となります。__range_adaptor_closure::operator|での*thisとは使われ方に応じてそのどちらかの型であり、Rangeアダプタの処理は_Fnの関数呼び出し演算子に実装されていて(__range_adaptor_closure_t<_Fn>の場合は_Fnを継承することで実装していて)、thisパラメータ__closureはそれらを呼び出すことができます。2つ目のoperator|で使用されている__compose(f, g)f(g(arg))となるように関数合成を行うラッパ型のようです。

filterの実装で__range_adaptor_closure_tの初期化に使用されている__bind_back()std::bind_frontと同じことを逆順で行うもので、Rangeアダプタの実装簡略化のためにP2287で提案されているものでもあります。

自作viewのアダプト

各実装をみて見ると、割とそこそこ異なっていることが分かります。従って、自作viewをrangesの|にアダプトするには各実装に合わせたコードが必要になりそうです(共通化も可能だと思いますが、考えがまとまっていないので次回以降・・・)。

Rangeアダプタオブジェクトとか知らねえ!とりあえず|で繋げればいい!!っていう人向け

Rangeアダプタの性質を知り色々実装を見てみると、|につなぐだけなら簡単なことに気付けます。Rangeアダプタは必ず|の右辺に来て、左辺はrangeviewable_range)オブジェクトとなります。複数チェーンしている時でも、1つのrange | RAの結果はviewになります。つまり、rangeを左辺に受ける|の実装においては左辺のオブジェクトは常にviewable_rangeとなります。

それを例えば自作のxxx_viewに実装すると

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  namespace views {
    namespace detail {

      struct xxx_view_adoptor {

        // Rangeアダプタの主処理
        template<typename R>
        [[nodiscard]]
        constexpr auto operator(R&& r) const {
          // xxx_viewの生成処理
        }

        // range | RA -> view なパイプライン演算子
        template <std::ranges::viewable_range R>
          requires requires(R&& r, const xxx_view_adoptor& self) {
            self(std::forward<R>(r));
          }
        [[nodiscard]]
        friend constexpr std::ranges::view auto operator|(R&& r, const xxx_view_adoptor& self) noexcept(noexcept(self(std::forward<R>(r)))) {
          return self(std::forward<R>(r));
        }
      };

    }

    inline constexpr xxx_view_adoptor xxx;
  }
}

省略した部分を適切に整えさえすれば、このoperator|定義は全ての実装でrangesのパイプラインチェーンにアダプトすることができます(多分)。

ただしこのxxx_view_adoptorはRangeアダプタとして必要なことを何もしていないので、それ以外の保証はありません。未定義動作にはならないと思いますが、標準のRangeアダプタ/Rangeアダプタクロージャオブジェクトと同等の振る舞いはできないので本当にとりあえずの実装です。

GCC10

GCC10の場合は、_RangeAdaptorClosure/_RangeAdaptorを適切にラムダ式などで初期化し、そのラムダ式内にview生成処理を記述します。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  namespace views {

    // xxx_viewのRangeアダプタクロージャオブジェクト
    inline constexpr std::views::__adaptor::_RangeAdaptorClosure xxx
      = [] <viewable_range _Range> (_Range&& __r)
      {
        // xxx_viewの生成処理
      };

    
    // xxx_viewのRangeアダプタオブジェクト
    inline constexpr std::views::__adaptor::_RangeAdaptor xxx
      = [] <viewable_range _Range, typename _Pred> (_Range&& __r, _Pred&& __p)
      {
          // xxx_viewの生成処理
      };
  }
}

GCC11

GCC11の場合も_RangeAdaptorClosure/_RangeAdaptorを使用するのですがラムダ式は使用できず、別にRangeアダプタ(クロージャ)型を定義してそこで継承して使用する必要があります。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor : std::views::__adaptor::_RangeAdaptor<xxx_adoptor>
      {
        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        // Rangeアダプタの部分適用共通処理を有効化
        using _RangeAdaptor<xxx_adoptor>::operator();

        // よくわからない場合は定義しない方がいいかもしれない
        static constexpr int _S_arity = 2;  // 入力rangeも含めた引数の数
        static constexpr bool _S_has_simple_extra_args = true;
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : std::views::__adaptor::_RangeAdaptorClosure  // これはCRTPではない
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }

        // _S_arityはクロージャオブジェクトの場合は不要らしい

        static constexpr bool _S_has_simple_call_op = true;
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

GCC10と11の間で使用法が結構変わっているのが地味に厄介かもしれません。GCCの場合はどちらでもRangeアダプタの引数事前適用を実装する必要がありません。

MSVC

MSVCの場合は_Pipe::_Baseを使用します。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor {

        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        template <typename Arg>
            requires std::constructible_from<std::decay_t<Arg>, Arg>
        constexpr auto operator()(Arg&& arg) const {
          // Rangeアダプタの引数事前適用処理
          return std::ranges::_Range_closure<xxx_adoptor, std::decay_t<Arg>>{std::forward<Arg>(arg)};
        }
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : public std::ranges::_Pipe::_Base<xxx_adoptor_closure>
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

MSVCの場合、Rangeアダプタの追加の引数を事前適用する処理を自分で記述する必要があります。そこでは_Range_closureを使用することでほぼ省略可能です。Rangeアダプタ型の場合はこれだけでよく、Rangeアダプタクロージャオブジェクト型の場合は_Pipe::_Baseを継承する必要があります。

clang

clangの場合、MSVCとほぼ同じ記述となり、使用するものが異なるだけです。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor {

        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        template <typename Arg>
            requires std::constructible_from<std::decay_t<Arg>, Arg>
        constexpr auto operator()(Arg&& arg) const {
          // Rangeアダプタの引数事前適用処理
          return std::__range_adaptor_closure_t(std::__bind_back(*this, std::forward<Arg>(arg)));
        }
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : public std::__range_adaptor_closure<xxx_adoptor_closure>
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

clangもMSVCの場合同様に、Rangeアダプタの追加の引数を事前適用する処理を自分で記述する必要があります。とはいえ内部実装を流用すればほぼ定型文となり、Rangeアダプタクロージャオブジェクト型の場合も__range_adaptor_closureを継承するだけです。

C++23から

これら実装を見ると、自作のview型を標準のものと混ぜてパイプで使用することはあまり想定されていなかったっぽいことが察せられます。そもそもviewを自作することってそんなにある?ということは置いておいて、この事態はあまり親切ではありません。

これまでもちらちら出ていましたが、この状況はP2287の採択によってC++23で改善されています。それによって、MSVC/clangの実装とほぼ同等に使用可能なユーティリティstd::ranges::range_adaptor_closurestd::bind_backが用意されます。これを利用すると次のように書けるようになります。

namespace myrange {

  template<std::ranges::view V>
  class xxx_view;

  // Rangeアダプタの場合
  namespace views {

    namespace detail {
      
      struct xxx_adoptor {

        template<std::ranges::viewable_range R, typename... Args>
        constexpr auto operator()(R&& r, Args&&... args) const {
          // xxx_viewの生成処理
        }

        template <typename Arg>
            requires constructible_from<decay_t<Arg>, Arg>
        constexpr auto operator()(Arg&& arg) const {
          // Rangeアダプタの引数事前適用処理
          return std::ranges::range_adaptor_closure(std::bind_back(*this, std::forward<Arg>(arg)));
        }
      };
    }

    inline constexpr detail::xxx_adoptor xxx{};
  }

  // Rangeアダプタクロージャの場合
  namespace views {

    namespace detail {
        
      struct xxx_adoptor_closure : public std::ranges::range_adaptor_closure<xxx_adoptor_closure>
      {
        template<std::ranges::viewable_range R>
        constexpr auto operator()(R&& r) const {
          // xxx_viewの生成処理
        }
      };
    }

    inline constexpr detail::xxx_adoptor_closure xxx{};
  }
}

これはC++23以降の世界で完全にポータブルです。ただし、MSVC/clang同様に、Rangeアダプタの追加の引数を事前適用する処理を自分で記述する必要があります。とはいえそれはやはりほぼ定型文まで簡略化されます。

参考文献

この記事のMarkdownソース