次期C++2a(C++20)標準規格に向けて採択された コンセプト(concept) についてメモ。*1
本記事の内容は(PDF)P0734R0 Wording Paper, C++ extensions for Conceptsに基づく。
要約:
- 新しいキーワード:
concept
,requires
- 新しい構文:コンセプト定義, requires式, requires節
- コンセプト(concept) == テンプレートパラメータに対する制約(constraint)
- コンセプト(concept) != 型(type)
- 関数オーバーロード解決では、関連付けられた制約(associated constraints)が考慮される。("concept-based overloading")
- 注意:P0734R0はコンセプトに関するC++言語仕様のみを規定し、具体的なコンセプトは何も定義しない。コンセプトライブラリは "Ranges TS"*2 で検討が行われている。
2019-01-11追記:C++2aに向けた提案文書P1141R2が採択され、関数テンプレートパラメータへの制約指定に関する短縮構文が追加される。(→id:yohhoy:20190111)
コンセプト(concept)
「コンセプト(concept)」は、C++の新しいエンティティ(entity)として追加される*3。コンセプトは “ある型が満たすべき制約(constraint)の集合” に名前をつけたものであり、新キーワード concept
を用いてテンプレート定義によく似た構文で定義される。コンセプト名に型を指定したものは、bool型のprvalueに評価される。*4
#include <type_traits> // is_integral_v // ある型パラメータT に対する コンセプトC0 を定義 template <typename T> concept C0 = std::is_integral_v<T>; // 型Tは整数型であること // 型パラメータパックTs に対する コンセプトCV を定義 template <typename... Ts> concept CV = sizeof...(Ts) == 2; // パックTsの要素数が2であること static_assert( C0<char> ); // OK static_assert( !C0<double> ); // OK static_assert( CV<float,double> ); // OK static_assert( !CV<int,int,int> ); // OK
コンセプト定義(concept-definition)では、コンセプトのプロトタイプパラメータ(prototype parameter)に対する制約式(constraint-expression)を指定する*5。制約式では &&
や ||
を用いて、制約に対する論理演算*6を指定できる。
template <typename T> concept A = /* 制約式 */; // コンセプトA template <typename T> concept B = /* 制約式 */; // コンセプトB // コンセプトC: 型TはコンセプトAかつコンセプトBの両方を満たす template <typename T> concept C = A<T> && B<T>; // コンセプトD: 型TがコンセプトCを満たす または 型UがコンセプトCを満たす template <typename T, typename U> concept D = C<T> || C<U>;
ノート:C++14現在のISO/IEC TS 19217:2015(通称 "Concepts TS")では、コンセプト定義は変数テンプレートや関数テンプレート風の構文であった。Concepts TSではキーワードbool
明示が必要だが、C++2aコンセプトでは上記のように専用構文となるためbool
不要となる。
// Concepts TS: 変数テンプレートの構文 template <typename T> concept bool C0 = sizeof(T) == 1; // Concepts TS: 関数テンプレートの構文 template <typename T> concept bool C0() { return sizeof(T) == 1; }
requires式による要件(requirement)表現
コンセプト定義に与える制約式では、新キーワード requires
を用いた「requires式(requires-expression)」が利用できる。
requires式は 1)“ある式が有効”、2)“ある型が存在する”、3a)“ある式が有効で、評価結果は特定の型に変換可能”、3b)“ある関数呼び出しが有効で、例外送出しない”、3c)“ある式が有効で、評価結果は別コンセプトを満たす型に推論可能”、4)“ある型が別コンセプトを満たす” といった要件(requirement)を、requires式の本体部に複数個並べて表現する。全ての要件を満たす(satisfied)場合にかぎり、requires式はtrueとなる。(requires式の本体部が実行されるわけではない)
// 1) 型T に対する制約を表す コンセプトC1 template <typename T> concept C1 = requires (T a, T b) { // 型Tの2変数a, bに対して... a + b; // 型Tの2変数に対する加算演算+が有効 a - b; // 型Tの2変数に対する減算演算-が有効 }; // 2) 型T に対する制約を表す コンセプトC2 template <typename T> concept C2 = requires { // (requires変数リストは省略可) typename T::inner; // 型Tは入れ子の型T::innerを持つ typename S<T>; // クラステンプレートSの特殊化S<T>が有効 }; // 3) 型T に対する制約を表す コンセプトC3 template <typename T> concept C3 = requires (T x) { // 型Tの変数xに対して... { !x } -> bool; // a) 型Tの変数に対する単項否定演算!が有効 // かつ その評価結果はbool型に変換可能 { f(x) } noexcept; // b) 式f(x)が有効 かつ 例外送出しない { g(x) } noexcept -> int; // b) 式g(x)が有効 かつ 例外送出せず // かつ その評価結果はint型へ変換可能 { h(x) } -> const C2&; // c) 式h(x)が有効 かつ その評価結果は架空の // 関数テンプレートFへの実引数として指定できる // template<C2 U> void F(const U&); }; // 4) 入れ子のコンセプトを含む コンセプトC4 template <typename T> concept C4 = requires { requires C3<T>; // 型TはコンセプトC3を満たす //... };
requires式の本体部における構文では、下記の要件(requirement)を記述可能となっている(簡略版):
- 1) simple-requirement: 式
;
- 2) type-requirement:
typename
型名;
- 3) compound-requirement:
{
式}
noexcept
opt;
*7{
式}
noexcept
opt->
型名;
{
式}
noexcept
opt->
制約付きパラメータ(constrained-parameter);
- 4) nested-requirement:
requires
制約式(constraint-expression);
テンプレートへの制約の関連付け
テンプレート宣言/定義においてテンプレートパラメータにコンセプトを適用するため、3種類の構文が追加される:(Cは関数テンプレートまたは関数に対してのみ有効)
- A) template宣言のテンプレートパラメータリストにおいて、
typename
/class
キーワードや型名の代わりに コンセプト名 を用いる。*8 - B) template宣言に続いて「requires節(requires-clause)」を指定する。
- C) 関数テンプレート/関数のプロトタイプ末尾に「末尾requires節(trailing requires-clause)」を指定する。
// 型パラメータTに対する コンセプトC を定義 template <typename T> concept C = /*...*/; // 通常の関数テンプレート template <typename T> // 型テンプレートパラメータT; 制約なし void func0t(T); template <int N> // 非型テンプレートパラメータN; 制約なし void func0n(); // A) 制約付きパラメータ(constrained-parameter)による制約 template <C T> void funcA(T); // B) requires節(requires-clause)による制約 template <typename T> requires C<T> void funcB(T); // C) 末尾requires節(trailing requires-clause)による制約 template <typename T> void funcC(T) requires C<T>;
funcA, funcB, funcCいずれの関数テンプレートも、制約 C<T>
が関連付けられる。制約は複数指定することもでき、下記funcDは制約 C1<T>∧C2<T>∧C3<T>
が関連付けられる。
template <typename T> concept C1 = /*...*/; template <typename T> concept C2 = /*...*/; template <typename T> concept C3 = /*...*/; template<C1 T> void funcD(T) requires C2<T> && C3<T>;
関数テンプレートに対して “関連付けられた制約(associated constraints)” が存在する場合、その制約が満たされた(satisfied)ときに限って関数オーバーロード候補に列挙される。また、異なる制約間には半順序(partial ordering)が定義され、関数オーバーロード解決の順序に影響を与える。関数オーバーロード解決の詳細仕様はヽ(・∀・ヽ) 理解!! (ノ・∀・)ノ 不能!!
// P0734R0 17.10.4/p5 Example template<typename T> concept C1 = requires(T t) { --t; }; template<typename T> concept C2 = C1<T> && requires(T t) { *t; }; template<C1 T> void f(T); // #1 template<C2 T> void f(T); // #2 template<typename T> void g(T); // #3 template<C1 T> void g(T); // #4 f(0); // selects #1 f((int*)0); // selects #2 g(true); // selects #3 because C1<bool> is not satisfied g(0); // selects #4
関連URL
*1:広く合意された対訳語は存在しないため、本文中では日本語直訳+英語表記を併記している。
*2:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4685.pdf
*3:コンセプト(concept)以外のC++エンティティ:value, object, reference, function, enumerator, type, class member, bit-field, template, template specialization, namespace, parameter pack
*4:P0734R0でも "evaluate(評価)" と表現しているが、実行時ではなくコンパイル時に行われる処理。またコンセプト(concept)はインスタンス化されないため、制約 C0<char> に対応する実体が存在するわけではない。
*5:P0734R0 17.5.6/p1では "A concept is a template that defines constraints on its template arguments." と定義されており、コンセプトはテンプレートの一種となっている。プロトタイプパラメータ(prototype parameter)=コンセプト定義のテンプレートパラメータ。
*6:ここでの && は合接(conjunction; ∧)、|| は離接(disjunction; ∨)とよばれる論理演算。この “制約に対する論理演算” は通常の論理演算子(logical OR/AND)とは別物として定義されるが、実用上は同義との理解で差し支えないはず。
*7:2019-08-27追記:2019年会合において提案文書P1452R2が採択され、requires式 compound-requirement において型名のみ後置する構文は削除された。C++2a標準コンセプトを利用して、T型を明示する代わりに std::same_as<T> または std::convertible_to<T> 制約として記述する。
*8:本文中では説明のため短いコンセプト名(Cなど)を用いている。実用上は制約条件を適切に表現でき、かつコンセプト(concept)であることが読み取れる名前をつけること。