Constraints and concepts (since C++20)
クラステンプレート 、 関数テンプレート ( ジェネリックラムダ を含む)、およびその他の テンプレート化された関数 (通常はクラステンプレートのメンバー)は、 制約 に関連付けられる場合があります。これはテンプレート引数に対する要件を指定し、最も適切な関数オーバーロードとテンプレート特殊化を選択するために使用できます。
このような 要件 の名前付き集合は concept と呼ばれます。各conceptはコンパイル時に評価される述語であり、制約として使用されるテンプレートのインターフェースの一部となります:
#include <cstddef> #include <concepts> #include <functional> #include <string> // コンセプト「Hashable」の宣言。任意の型「T」が以下の条件を満たす場合に充足される: // 型「T」の値「a」に対して、式 std::hash<T>{}(a) がコンパイルされ、 // その結果が std::size_t に変換可能であること template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // 制約付きC++20関数テンプレート: template<Hashable T> void f(T) {} // // 同じ制約を適用する代替方法: // template<typename T> // requires Hashable<T> // void f(T) {} // // template<typename T> // void f(T) requires Hashable<T> {} // // void f(Hashable auto /* parameter-name */) {} int main() { using std::operator""s; f("abc"s); // OK、std::stringはHashableを満たす // f(meow{}); // エラー:meowはHashableを満たさない }
制約違反はコンパイル時に、テンプレートのインスタンス化プロセスの早い段階で検出されるため、理解しやすいエラーメッセージが得られます:
std::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // コンセプトなしでの典型的なコンパイラ診断メッセージ: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 lines of output ... // // コンセプトありでの典型的なコンパイラ診断メッセージ: // error: cannot call std::sort with std::_List_iterator<int> // note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
コンセプトの意図は、構文的な制約(HasPlus、Array)ではなく、意味的なカテゴリ(Number、Range、RegularFunction)をモデル化することです。 ISO C++コアガイドライン T.20 によれば、「意味のあるセマンティクスを指定できる能力は、構文的制約とは対照的に、真のコンセプトを定義する特徴です。」
目次 |
コンセプト
コンセプトは名前付きの 要件 の集合である。コンセプトの定義は名前空間スコープで宣言されなければならない。
コンセプトの定義は以下の形式を持ちます
template <
テンプレート引数リスト
>
|
|||||||||
| attr | - | 任意の数の attributes のシーケンス |
// コンセプト template<class T, class U> concept Derived = std::is_base_of<U, T>::value;
コンセプトは再帰的に自身を参照することはできず、制約を課すこともできません:
template<typename T> concept V = V<T*>; // エラー: 再帰的なコンセプト template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // エラー: C1 T はコンセプト定義を制約しようとしている template<class T> requires C1<T> concept Error2 = true; // エラー: requires節はコンセプトを制約しようとしている
概念の明示的なインスタンス化、明示的特殊化、または部分特殊化は許可されていません(制約の元の定義の意味を変更することはできません)。
コンセプトはid式で名前を参照できます。id式の値は、制約式が満たされる場合は true となり、そうでない場合は false となります。
Conceptsは型制約内でも名前付け可能であり、その一部として
- 型テンプレートパラメータ宣言 ,
- プレースホルダ型指定子 ,
- 複合要件 .
type-constraint では、概念はそのパラメータリストが要求するよりも1つ少ないテンプレート引数を取ります。なぜなら、文脈から推論された型が概念の最初の引数として暗黙的に使用されるためです。
template<class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T は Derived<T, Base> によって制約される
制約条件
制約は、テンプレート引数に対する要件を指定する論理演算とオペランドのシーケンスです。これらは requires expressions 内、またはコンセプトの本体として直接記述できます。
制約には 三種類 (C++26まで) 四種類 (C++26以降) あります:
|
4)
折り畳み展開された制約
|
(C++26以降) |
宣言に関連する制約は、以下の順序でオペランドが並ぶ論理AND式を 正規化 することによって決定されます:
- 制約付きの 型テンプレートパラメータ または制約付きの プレースホルダ型 で宣言された定数テンプレートパラメータごとに導入される制約式(出現順);
- テンプレートパラメータリストの後の requires 節 内の制約式;
- 省略関数テンプレート 宣言において、制約付きの プレースホルダ型 を持つ各パラメータに対して導入される制約式;
- 末尾の requires 節 内の制約式。
この順序は、制約が充足されているかどうかをチェックする際に制約がインスタンス化される順序を決定します。
再宣言
制約付き宣言は、同じ構文形式を使用してのみ再宣言できます。診断は必要ありません:
// 最初の2つのfの宣言は問題ありません template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // OK、再宣言 // この3番目の、論理的には等価だが構文的に異なるfの宣言を含めると // 不適格、診断不要となります template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // 以下の2つの宣言は異なる制約を持ちます: // 最初の宣言は Incrementable<T> && Decrementable<T> // 2番目の宣言は Decrementable<T> && Incrementable<T> // 論理的には等価であるにもかかわらず template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // 不適格、診断不要
接続詞
2つの制約の論理積は、制約式内で
&&
演算子を使用して形成されます:
template<class T> concept Integral = std::is_integral<T>::value; template<class T> concept SignedIntegral = Integral<T> && std::is_signed<T>::value; template<class T> concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
2つの制約の論理積は、両方の制約が満たされた場合にのみ満たされます。論理積は左から右へ評価され、短絡評価されます(左側の制約が満たされない場合、右側の制約へのテンプレート引数の置き換えは試行されません:これにより、即時コンテキスト外での置き換えによる失敗を防ぎます)。
template<typename T> constexpr bool get_value() { return T::value; } template<typename T> requires (sizeof(T) > 1 && get_value<T>()) void f(T); // #1 void f(int); // #2 void g() { f('A'); // OK、#2を呼び出す。#1の制約をチェックする際、 // 'sizeof(char) > 1'が満たされないため、get_value<T>()はチェックされない }
論理和
2つの制約の論理和は、制約式内で
||
演算子を使用して形成されます。
2つの制約の論理和は、いずれかの制約が満たされた場合に満たされます。論理和は左から右に評価され、短絡評価されます(左側の制約が満たされた場合、右側の制約へのテンプレート引数の代入は試行されません)。
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
アトミック制約
アトミック制約は、式 E と、 E 内に現れるテンプレートパラメータから、制約対象エンティティのテンプレートパラメータを含むテンプレート引数へのマッピングで構成され、これを パラメータマッピング と呼びます。
アトミック制約は 制約正規化 の過程で形成されます。 E は論理AND式や論理OR式になることはありません(これらはそれぞれ連言と選言を形成します)。
アトミック制約の充足は、パラメータマッピングとテンプレート引数を式 E に代入することで検査されます。代入の結果が無効な型または式となる場合、制約は充足されません。それ以外の場合、 E は、左辺値から右辺値への変換を経た後、 bool 型のprvalue定数式でなければならず、その評価結果が true である場合にのみ制約は充足されます。
E の置換後の型は厳密に bool でなければなりません。変換は許可されません:
template<typename T> struct S { constexpr operator bool() const { return true; } }; template<typename T> requires (S<T>{}) void f(T); // #1 void f(int); // #2 void g() { f(0); // エラー: #1をチェックする際、S<int>{}がbool型を持たないため、 // #2がより適切なマッチであるにもかかわらず }
二つのアトミック制約は、ソースレベルで同じ式から形成され、それらのパラメータマッピングが等価である場合、 同一 と見なされます。
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_cat = true; template<class T> concept Meowable = is_meowable<T>; template<class T> concept BadMeowableCat = is_meowable<T> && is_cat<T>; template<class T> concept GoodMeowableCat = Meowable<T> && is_cat<T>; template<Meowable T> void f1(T); // #1 template<BadMeowableCat T> void f1(T); // #2 template<Meowable T> void f2(T); // #3 template<GoodMeowableCat T> void f2(T); // #4 void g() { f1(0); // エラー、曖昧: // MeowableとBadMeowableCat内のis_meowable<T>は同一でない // 個別のアトミック制約を形成する(従って互いに包含しない) f2(0); // OK、#4を呼び出す、#3よりも制約が強い // GoodMeowableCatはis_meowable<T>をMeowableから取得している }
畳み込み展開制約
畳み込み展開制約
は、制約
パック展開パラメータの要素数を N とします:
template <class T> concept A = std::is_move_constructible_v<T>; template <class T> concept B = std::is_copy_constructible_v<T>; template <class T> concept C = A<T> && B<T>; // C++23では、これらの2つのg()オーバーロードは同一でなく、互いに包含しない // 個別のアトミック制約を持つため、g()の呼び出しは曖昧です // C++26では、畳み込みが展開され、オーバーロード#2の制約(ムーブとコピーの両方が必要)が // オーバーロード#1の制約(ムーブのみ必要)を包含します template <class... T> requires (A<T> && ...) void g(T...); // #1 template <class... T> requires (C<T> && ...) void g(T...); // #2
|
(C++26以降) |
制約正規化
制約正規化 は、制約式を原子制約の連言と選言の列に変換する処理です。式の 正規形 は以下のように定義されます:
- 式 ( E ) の正規形は、 E の正規形である。
- 式 E1 && E2 の正規形は、 E1 と E2 の正規形の論理積である。
- 式 E1 || E2 の正規形は、 E1 と E2 の正規形の論理和である。
-
式
C
<
A1, A2, ... , AN
>
の正規形(ここで
Cはコンセプト名)は、Cの制約式の正規形であり、Cの各アトミック制約のパラメータマッピングにおいて、Cの対応するテンプレートパラメータにA1、A2、...、ANを代入した後のものである。パラメータマッピングへの代入によって無効な型または式が生じた場合、プログラムは不適格であり、診断は不要である。
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // OK: 以下の論理和に正規化される // - T::value (マッピング T -> U*) と // - true (空のマッピング) // すべてのポインタ型に対して T::value が不適格であっても、 // マッピングに無効な型は存在しない template<typename V> concept C = B<V&>; // 以下の論理和に正規化される // - T::value (マッピング T-> V&*) と // - true (空のマッピング) // マッピングで無効な型 V&* が形成される => 不適格 NDR
|
(C++26以降) |
-
その他の式
E
の正規形は、式が
E
であり、パラメータマッピングが恒等マッピングであるアトミック制約です。これには、
畳み込み式
すべてが含まれます(
&&または||演算子を畳み込むものも含む)。
ユーザー定義の
&&
または
||
のオーバーロードは、制約の正規化に影響を与えません。
requires clauses
キーワード requires は requires 節 を導入するために使用され、テンプレート引数または関数宣言に対する制約を指定します。
template<typename T> void f(T&&) requires Eq<T>; // 関数宣言子の最後の要素として現れることができる template<typename T> requires Addable<T> // またはテンプレートパラメータリストの直後 T add(T a, T b) { return a + b; }
この場合、キーワード requires は何らかの定数式が後に続く必要があります(したがって requires true と記述することが可能です)。しかし意図としては、前述の例のように名前付きコンセプト、または名前付きコンセプトの連言/選言、あるいは requires式 が使用されることです。
式は以下のいずれかの形式でなければなりません:
- 一次式(例: Swappable < T > 、 std:: is_integral < T > :: value 、 ( std:: is_object_v < Args > && ... ) 、または任意の括弧で囲まれた式)
-
&&演算子で結合された一次式のシーケンス -
||演算子で結合された前述の式のシーケンス
template<class T> constexpr bool is_meowable = true; template<class T> constexpr bool is_purrable() { return true; } template<class T> void f(T) requires is_meowable<T>; // OK template<class T> void g(T) requires is_purrable<T>(); // エラー: is_purrable<T>() は一次式ではない template<class T> void h(T) requires (is_purrable<T>()); // OK
制約の部分順序付け
さらなる分析の前に、制約はすべての名前付きコンセプトの本体とすべての requires式 を置換することによって 正規化 され、残るのはアトミック制約上の連言と選言のシーケンスとなります。
制約
P
は、制約
Q
を
包含する
と言われます。これは、
P
が
含意する
Q
が、PとQの原子制約の同一性まで証明できる場合です。(型と式は等価性について分析されません:
N > 0
は
N >= 0
を包含しません)。
具体的には、まず
P
が選言標準形に変換され、
Q
が連言標準形に変換されます。
P
が
Q
を包含するのは、次の場合に限ります:
-
Pの選言標準形におけるすべての選言節は、Qの連言標準形におけるすべての連言節を包含する。ここで、 -
選言節が連言節を包含するのは、選言節内に原子制約
Uが存在し、 連言節内に原子制約Vが存在して、UがVを包含する場合に限る。 -
原子制約
Aが原子制約Bを包含するのは、 前述 の規則を用いて 両者が同一である場合に限る。
|
(C++26以降) |
包含関係は制約の半順序を定義し、これは以下を決定するために使用されます:
- 非テンプレート関数に対するオーバーロード解決における 最適な実行可能候補
- オーバーロード集合における 非テンプレート関数のアドレス
- テンプレートテンプレート引数に対する最適なマッチ
- クラステンプレート特殊化の部分順序付け
- 関数テンプレートの 部分順序付け
|
このセクションは不完全です
理由:上記からのバックリンク |
宣言
D1
と
D2
が制約付きであり、
D1
の関連制約が
D2
の関連制約を包含する場合(あるいは
D2
が無制約の場合)、
D1
は
D2
と
少なくとも同等に制約されている
と言います。
D1
が
D2
と少なくとも同等に制約されており、かつ
D2
が
D1
と少なくとも同等に制約されていない場合、
D1
は
D2
より
より強く制約されている
と言います。
以下のすべての条件が満たされる場合、非テンプレート関数
F1
は非テンプレート関数
F2
よりも
部分順序付け制約が強い
:
- それらは同じパラメータ型リストを持つ (明示的なオブジェクトパラメータの型を省略した場合) explicit object parameters (since C++23) 。
- それらがメンバ関数である場合、両者は同じクラスの直接メンバである。
- 両方が非静的メンバ関数である場合、それらのオブジェクトパラメータの型は同じである。
-
F1はF2よりも制約が強い。
template<typename T> concept Decrementable = requires(T t) { --t; }; template<typename T> concept RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIteratorはDecrementableを包含するが、逆は成り立たない template<Decrementable T> void f(T); // #1 template<RevIterator T> void f(T); // #2, #1より制約が強い f(0); // intはDecrementableのみを満たすため、#1を選択 f((int*)0); // int*は両方の制約を満たすため、より制約の強い#2を選択 template<class T> void g(T); // #3 (制約なし) template<Decrementable T> void g(T); // #4 g(true); // boolはDecrementableを満たさないため、#3を選択 g(0); // intはDecrementableを満たすため、より制約の強い#4を選択 template<typename T> concept RevIterator2 = requires(T t) { --t; *t; }; template<Decrementable T> void h(T); // #5 template<RevIterator2 T> void h(T); // #6 h((int*)0); // 曖昧
注記
| 機能テストマクロ | 値 | 規格 | 機能 |
|---|---|---|---|
__cpp_concepts
|
201907L
|
(C++20) | 制約 |
202002L
|
(C++20) | 条件付きトリビアル 特殊メンバ関数 |
キーワード
不具合報告
以下の動作変更の欠陥報告書は、以前に公開されたC++規格に対して遡及的に適用されました。
| DR | Applied to | Behavior as published | Correct behavior |
|---|---|---|---|
| CWG 2428 | C++20 | コンセプトに属性を適用できなかった | 許可される |
関連項目
| Requires式 (C++20) | 型 bool のprvalue式を生成し、制約を記述する |