C++2a(C++20)標準ライブラリ入りが予定されている「範囲(Ranges)ライブラリ」のうち、クラステンプレート宣言がやたら入り組んでいる std::ranges::subrange を(ちょっとだけ)分解してみるお話です。
本記事では2019年6月現在のC++仕様草案 N4810 を参照します。
2020年9月追記:C++2a仕様の最終ドラフト N4861 では、std::ranges::subrange クラステンプレートの意味論は変わっていませんが、同クラステンプレート定義で利用するコンセプト名は変更されています。
範囲ライブラリ
C++2aで新たに標準入りした範囲ライブラリによって、従来C++標準ライブラリで提供されていたイテレータ(Iterator)よりも便利で安全なコード記述が可能となります(なるはずです)。単純化した説明では、これまではイテレータのペアとして表現せざるを得なかった “範囲(Range)” を、単一オブジェクトで表現できるようになります。本記事ではこれ以上深入りしません。
# include <iostream>
# include <vector>
# include <ranges>
namespace view = std::ranges::view;
auto ints = std::vector{0, 1, 2, 3, 4, 5};
auto even = [](int i){ return 0 == i % 2; };
auto square = [](int i) { return i * i; };
for (int i : ints | view::filter(even) | view::transform(square)) {
std::cout << i << ' '; // 0 4 16 を出力
}
本記事で取り上げる std::ranges::subrange クラステンプレート(以下subrange)は範囲ライブラリの一部として定義されます1。ただし通常のC++アプリケーションプログラマが直接利用するクラスというよりも、std::ranges::search関数などの探索系アルゴリズムが返す「部分範囲(subrange)」表現のためにライブラリ実装者が利用するクラスです。
2020年9月追記:std::ranges::subrange クラステンプレートは、C++17現在のイテレータ・ペアからC++2aの範囲(Range)へ変換するアダプタとしても利用できます。
subrangeクラステンプレート
C++2a標準ライブラリにおける subrange クラステンプレート宣言は下記コードの通りです。なんて分かりやすい!
namespace std::ranges {
template<Iterator I, Sentinel<I> S = I, subrange_kind K =
SizedSentinel<S, I> ? subrange_kind::sized : subrange_kind::unsized>
requires (K == subrange_kind::sized || !SizedSentinel<S, I>)
class subrange : public view_interface<subrange<I, S, K>> {
/*...*/
};
}
C++2a言語仕様で新たに導入される コンセプト(Concepts) に関する前提知識がないと全くの意味不明、むしろ知識があっても記号の羅列にしか見えないかもしれません。
subrangeクラステンプレート w/o コンセプト
ひとまずコンセプトに関する構文を無視して、C++17現在の言語機能のみで表現してみましょう。
namespace std::ranges {
template<class D> class view_interface;
enum class subrange_kind : bool { unsized, sized };
template<class I, class S = I, subrange_kind K = /*...*/>
class subrange : public view_interface<subrange<I, S, K>> {
/*...*/
};
}
だいぶスッキリしました。subrange クラステンプレートは3個のテンプレートパラメータI, S, Kをとり、2番目の型パラメータSと3番目の非型パラメータKにはデフォルト値が指定されています。
-
I= イテレータ型 -
S= 番兵型(通常はイテレータ型Iに等しい) -
K= 部分範囲の長さが有界(sized)か否か(unsized)を表す定数(通常はsized)
subrange は自身をテンプレート引数に指定した基底クラス view_interface<subrange<I, S, K>> から派生するCRTPイディオムを利用しています。view_interface<D> から継承したクラスは “Viewオブジェクト2”、つまり “軽量な範囲(Range)オブジェクト3” として扱えるようになります。本記事ではこれ以上深入りしません(2回目)。
subrangeクラステンプレート w/ コンセプト
あらためてC++2a標準ライブラリに立ち返りつつ、subrange クラステンプレートから参照される3つのコンセプト(Iterator, Sentinel, SizedSentinel)も引用しておきます。
namespace std {
template<class I> concept Iterator = /*...*/;
template<class S, class I> concept Sentinel = /*...*/;
template<class S, class I> concept SizedSentinel = /*...*/;
}
namespace std::ranges {
template<class D> class view_interface;
enum class subrange_kind : bool { unsized, sized };
template<Iterator I, Sentinel<I> S = I, subrange_kind K =
SizedSentinel<S, I> ? subrange_kind::sized : subrange_kind::unsized>
requires (K == subrange_kind::sized || !SizedSentinel<S, I>)
class subrange : public view_interface<subrange<I, S, K>> {
/*...*/
};
}
テンプレートパラメータリスト部でのコンセプト指定(template<C T>)は、「テンプレートパラメータ型TがコンセプトC<T>を満たす」ことを表現しており、requires節を用いたtemplate<class T> requires C<T>と同義です。
つまり subrange クラステンプレートは、テンプレートパラメータが下記3つの制約(constraints)を満たすときに限ってインスタンス化されます。
- 型テンプレートパラメータ
IがIterator<I>コンセプトを満たし、かつ - 型テンプレートパラメータ
SがSentinel<S, I>コンセプトを満たし、かつ - 非型テンプレートパラメータ
K==sizedもしくはSizedSentinel<S, I>コンセプトを満たさないとき。 - 非型テンプレートパラメータ
Kのデフォルト値は、SizedSentinel<S, I>コンセプトを満たす場合はsizedを、そうでなければunsizedをとる。
これで subrange クラステンプレート宣言を解釈できるようになりましたよね?本記事ではこれ以上深入りしません(3回目)
-
[range.subrange]/p1: "The
subrangeclass template combines together an iterator and a sentinel into a single object that models theViewconcept. Additionally, it models theSizedRangeconcept when the final template parameter issubrange_kind::sized." ↩ -
[view.interface]/p1: "The class template
view_interfaceis a helper for definingView-like types that offer a container-like interface." ↩ -
[range.req.general]/p2: "The
Viewconcept specifies requirements on aRangetype with constant-time copy and assign operations." ↩