yohhoyの日記

技術的メモをしていきたい日記

C++ Concepts(P0734R0)

次期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:
    • {} noexceptopt ;
    • {} noexceptopt -> 型名 ; *7
    • {} noexceptopt -> 制約付きパラメータ(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)であることが読み取れる名前をつけること。