[C++]C++23<ranges>のユーティリティ

C++23で追加された<ranges>関連の小さめのユーティリティをまとめておきます。ここには新しいファクトリ/アダプタやranges::toは含まれていません。ここで紹介するものは基本的にstd::ranges名前空間にありますが、名前空間指定を省略しています。

const_iterator_t/const_sentinel_t

const_iterator_trange型からその定数イテレータconst iterator)の型を取得するエイリアステンプレートです。const_sentinel_tはそれに対応する番兵型を取得するものです。これらは、iterator_t/sentinel_tの亜種です。

namespace std::ranges {
  template<range R>
  using const_iterator_t = const_iterator<iterator_t<R>>;

  template<range R>
  using const_sentinel_t = const_sentinel<sentinel_t<R>>;
}

定数イテレータはその要素が変更できないイテレータのことで、ほぼ間接参照結果がconst参照になっていると思って差し支えありません。const_sentinel_tで取得できる番兵型も意味合いは同様なのですが、通常番兵を間接参照することはほぼないのでこれはconst_iterator_tと対になるように用意されている側面が強いものです。

定義で使用されているconst_iterator/const_sentinelイテレータ(番兵)型を受けてそれを定数イテレータ(番兵)型に変換するエイリアステンプレートで、この変換に際しては入力イテレータ型の間接参照結果が既にconstではない場合にのみstd::basic_const_iteratorでラップしてイテレータを確実に定数化します。

const_iterator_t/const_sentinel_tで得られるイテレータ型は、C++23以降のstd::ranges::cbegin/std::ranges::cendで取得できるイテレータの型と一致します。

#include <ranges>
using namespace std::ranges;

template<range R>
void f(R& rng) {
  const_iterator_t<R> cit = cbegin(rng);  // Rに関わらずok
  const_sentinel_t<R> cse = cend(rng);    // Rに関わらずok

  *cit = ...; // ng(ほとんどの場合)
}

ただし例外として、間接参照結果がprvalue(すなわち非参照)であるようなイテレータ型(range型)に対するconst_iterator/const_iterator_tの結果のイテレータ型は、間接参照結果がconst参照ではなくprvalueのままとなります(const修飾がある場合は外されます)。これは、間接参照結果のprvalueをどう変更したとしても元のrangeの要素を変更してはいないためと、prvalueの結果をconst参照化するとダングリング参照になるためです。

#include <ranges>
using namespace std::ranges;

// iota_viewの間接参照結果型は要素型のprvalue
void f(iota_view<int> vi) {
  const_iterator_t<iota_view<int>> cit = cbegin(vi);
  const_sentinel_t<iota_view<int>> cse = cend(vi);

  static_assert(std::same_as<decltype(*cit), int>); // パスする

  *cit = 10;  // 要素型が組み込み型の場合ng、クラス型の場合はエラーにならない場合がある
}

// 間接参照結果がstringのprvalueの場合
template<range R>
  requires std::same_as<range_reference_t<R>, std::string>
void g(R& rng) {
  const_iterator_t<R> cit = cbegin(vi);
  const_sentinel_t<R> cse = cend(vi);

  *cit = "str"; // ok、ただしこの変更は観測されない
}

他の特殊な場合として、views::zipのような特殊なイテレータ型(値型と参照型の関係が複雑なイテレータ型)の場合は、const_iterator/const_iterator_tを通した後の間接参照結果型は少し複雑な変換を受けます(参照型std::tuple<T1&, T2&, ...>に対して、std::tuple<const T1&, const T2&, ...>のようになる)。

とはいえそのような特殊なイテレータ型の場合はcommon_referenceのカスタマイズなどを通して適切にconst対応が取られているはずなので、結果的には、const_iterator/const_iterator_tを通したイテレータ型では常にその要素は変更されない(できない)とみなすことができます。よって、これと一致するstd::ranges::cbegin/std::ranges::cendで得られるイテレータも常に定数イテレータになるようになります(C++20時点では、必ずしも定数イテレータを得られない場合がありました)。

なお、これらのエイリアステンプレートによる定数イテレータの参照型の決定は最終的にstd::iter_const_reference_tによって行われます。これについては以前の記事を参照ください

range_const_reference_t

range_const_reference_trangeRから、その間接参照結果をconst化した型を取得するエイリアステンプレートです。

namespace std::ranges {
  template<range R>
  using range_const_reference_t = iter_const_reference_t<iterator_t<R>>;
}

この型は、先程のconst_iterator_tで得られる定数イテレータの間接参照結果の型となります。

#include <ranges>
using namespace std::ranges;

template<range R>
void f(R& rng) {
  const_iterator_t<R> cit = cbegin(vi);

  range_const_reference_t<R> cr = *cit;  // ok
}

ただし、前述のようにprvalueconst化した型はそのままprvalueとなるほか、viws::zipなど要素型が特殊な場合もあるため、range_const_reference_tによって得られる型は参照型ではない場合があり、常にconstであるわけでもありません。それでも、const_iterator_tが必ず定数イテレータを取得するように、range_const_reference_tもまたそれを通して元の範囲の要素を変更できない型を示します。

costant_range

costant_rangeはその要素を変更できないrangeを表すコンセプトです。

namespace std::ranges {
  template<class T>
  concept constant_range = input_range<T> && constant-iterator<iterator_t<T>>;
}

定義はinput_rangeかつそのイテレータconstant-iteratorであることを要求し、constant-iteratorイテレータが定数イテレータであることを表す説明専用のコンセプトです。

このコンセプトは主に、rangeを受ける場所で受け取ったrangeの要素を変更しない場合に使用すると良いでしょう。

#include <ranges>
using namespace std::ranges;

// constant_rangeコンセプトで制約することで、受け取ったrangeの要素を変更しないことを表明
void f(constant_range auto&& rng) {
  // constant_rangeを満たしているため、内部では要素を変更しようとしてもできない
  auto it = begin(rng);
  *it = ...; // ngもしくは無意味

  ...
}

int main() {
  std::vector<int> vec = {1, 2, 3, 4, 5};
  const auto& crv = vec;

  f(vec); // ng
  f(crv);  // ok
}

costant_rangeコンセプトは構文的にイテレータを介して要素が変更不可能であることを要求しているため、関数の実装側もcostant_rangeとして受け取った範囲の要素を変更しようとしても変更できません。

costant_rangeで制約されているところに渡すために範囲を手軽にcostant_range化するには、views::as_constを使用します。

#include <ranges>
using namespace std::ranges;

void f(constant_range auto&& R) {
  ...
}

int main() {
  std::vector<int> vec = {1, 2, 3, 4, 5};

  f(vec); // ng
  f(vec | views::as_const); // ok
}

views::as_constは、入力rangeを単にconst化したり、そのイテレータstd::basic_const_iteratorでラップするなどして、入力rangeconstant_rangeへ変換します。

range_adaptor_closure

ここまでのものは全て定数イテレータに関連するものでしたが、これは上のものとは無関係なものです。

range_adaptor_closureは、レンジアダプタを自作する際に標準のアダプタと|で接続できるようにするためのクラス型です。CRTPによって継承して利用します。

namespace std::ranges {
  template<class D>
    requires is_class_v<D> && same_as<D, remove_cv_t<D>>
  class range_adaptor_closure { }; 
}

まず、これを使用せず何も考えずに自作のレンジアダプタを作成すると、標準のアダプタなら可能なパイプライン演算子|)を使用した入力や合成ができません。

#include <ranges>
using namespace std::ranges;

// 自作のレンジアダプタ型
// 特にパイプのための実装をしていないとする
class my_original_adaptor {
  ...
};

inline constexpr my_original_adaptor my_adaptor{};

int main() {
  std::vector vec = {...};

  // パイプでviewを入力できない
  view auto v1 = vec | my_adaptor; // ng
  view auto v2 = vec | views::take(2) | my_adaptor;  // ng

  // レンジアダプタの合成もできない
  auto raco1 = views::take(2) | my_adaptor;  // ng
  auto raco2 = my_adaptor | views::take(2);  // ng
}

これを可能とするには標準ライブラリ実装が用いているのと同様の方法によってパイプライン演算子を有効化しなければなりませんが、それは実装詳細であり公開されているものではなく、各実装でバラバラです。そこで、range_adaptor_closureを使用すると統一的かつ実装詳細を気にしない方法でパイプライン演算子を有効化することができます。

#include <ranges>
using namespace std::ranges;

// 自作のレンジアダプタ型
// CRTPによってrange_adaptor_closureを継承する
class my_original_adaptor : range_adaptor_closure<my_original_adaptor> {
  ...
};

inline constexpr my_original_adaptor my_adaptor{};

int main() {
  std::vector vec = {...};

  // パイプでviewを入力できるようになる
  view auto v1 = vec | my_adaptor; // ok
  view auto v2 = vec | views::take(2) | my_adaptor;  // ok

  // レンジアダプタの合成も有効化される
  auto raco1 = views::take(2) | my_adaptor;  // ok
  auto raco2 = my_adaptor | views::take(2);  // ok
}

ここでは自作のレンジアダプタmy_original_adaptorの実装詳細を省略していますが、当然それはパイプライン演算子対応以外の部分はきちんとレンジアダプタとして実装されている必要があります。

range_adaptor_closureはこのように、本来とても複雑なパイプライン演算子対応を自動化してくれるものです。レンジアダプタを自作することは稀だと思われるためあまり使用機会はないかもしれませんが、レンジアダプタを自作する場合は非常に有用なものとなるでしょう。

ただ、range_adaptor_closureはその名の通りレンジアダプタクロージャオブジェクト型に対してパイプライン演算子を有効化することしかしません。追加の引数が必要なレンジアダプタオブジェクト型において、入力range以外の引数を予め受けておいてレンジアダプタクロージャオブジェクトを生成する部分についてはrange_adaptor_closureはもちろん、他にも特にサポートがありません。

#include <ranges>
using namespace std::ranges;

int main() {
  std::vector vec = {...};
  
  // views::commonはレンジアダプタクロージャオブジェクト
  view auto v1 = vec | views::common;

  // 追加の引数が必要なものはレンジアダプタオブジェクト
  view auto v2 = vec | views::take(5);
  view auto v3 = vec | views::filter([](auto v) { ... });
  view auto v4 = vec | views::transform([](auto v) { ... });

  // レンジアダプタオブジェクトに追加の引数を予め充填したものはレンジアダプタクロージャオブジェクト
  auto raco1 = views::take(5);
  auto raco2 = views::filter([](auto v) { ... });
  auto raco3 = views::transform([](auto v) { ... });

  // パイプライン演算子はレンジアダプタクロージャオブジェクトに対して作用する
  view auto v5 = vec | raco1; // v2と同じ意味
  view auto v6 = vec | raco2; // v3と同じ意味
  view auto v7 = vec | raco3; // v4と同じ意味
  auto raco4 = raco1 | raco2 | raco3;
}

レンジアダプタを自作する場合、必ずしもレンジアダプタクロージャとして実装できない場合が容易に考えられ、その場合は追加の引数を保存してレンジアダプタクロージャオブジェクトを生成するという部分を実装しなければなりません。

幸いなことに、C++23から追加されたstd::bind_back()std::bind_front()の逆順版)を使用すると、追加の引数の順番を保った保存の部分を委任することができます。

#include <ranges>
using namespace std::ranges;

// 自作のレンジアダプタクロージャ型
template<typename F>
struct my_closure_adaptor : range_adaptor_closure<my_closure_adaptor<F>> {
  F f;

  view auto operator()(viewable_range auto&& input) const {
    return f(input);  // bind_back()でラッピングされたcallbleに引数のrangeを入力しレンジアダプタを実行する
  }
};

// 自作のレンジアダプタ型(not クロージャ)
class my_original_adaptor {

  ...
  
  template<typename... Args>
  view auto operator()(viewable_range auto&& input, Args&&... args) const {
    // 入力rangeと必要な引数がすべてそろった状態で、レンジアダプタを実行し結果のviewを返す
    return ...;
  }
  
  // 追加の引数を受けてレンジアダプタクロージャオブジェクトを返す
  template<typename... Args>
  auto operator()(Args&&... args) const {
    // bind_back()で自身と追加の引数をラッピングし、レンジアダプタクロージャオブジェクトを作成
    return my_closure_adaptor{ .f = std::bind_back(*this, std::forward<Args>(args)...)};
  }
};

inline constexpr my_original_adaptor my_adaptor{};

int main() {
  std::vector vec = {...};

  // my_original_adaptorが追加の引数として整数を1つ受け取るとすると
  view auto v1 = vec | my_adaptor(1); // ok
  view auto v2 = vec | views::take(2) | my_adaptor(2);  // ok
  auto raco = my_adaptor(1);  // ok
  view auto v3 = vec | raco;  // ok、v1と同じ意味
}

my_original_adaptorはここでも実装を省略していますが、そこについては適切に実装されている必要があります(何かいい例があればいいのですが・・・)。ただ、おそらくはほとんどの場合は、この例のように必要なものをすべて受けてレンジアダプタとしての処理を実行する関数呼び出し演算子と、追加の引数だけを受けて対応するレンジアダプタクロージャオブジェクトを作成する関数呼び出し演算子の2つを記述することになると思われます。

おそらく、このmy_closure_adaptor(汎用的なレンジアダプタクロージャオブジェクト型)のようなものは自作のレンジアダプタとは別で作成できて、かつこの例のような典型的な実装になるはずです。そのため、自作のレンジアダプタ毎にこのようなものを作る必要は無いでしょう。この部分がC++23で追加されなかったのは、この部分の最適な実装がまだ確立されていないためだったようです。

宣伝ですが、これらレンジアダプタ(クロージャ)オブジェクトを簡易に作成できるC++20/C++23のライブラリを作っていました。range_adaptor_closurestd::bind_back相当のものをC++20で使用できるほか、my_closure_adaptor相当のものも用意しており、より簡易にレンジアダプタを作成できるようになります。

参考文献

この記事のMarkdownソース