制約とコンセプト (C++20 以降)
クラステンプレート、関数テンプレート (汎用ラムダを含む)、およびその他のテンプレート化された関数 (通常はクラステンプレートのメンバー) は、テンプレート引数に対する要件を指定する「制約」と関連付けられる場合があります。この制約は、最も適切な関数オーバーロードやテンプレート特殊化を選択するために使用できます。
このような要件の命名されたセットを「コンセプト」と呼びます。各コンセプトはコンパイル時に評価される述語であり、制約として使用されるテンプレートのインターフェースの一部となります。
#include <cstddef> #include <concepts> #include <functional> #include <string> // Declaration of the concept “Hashable”, which is satisfied by any type “T” // such that for values “a” of type “T”, the expression std::hash<T>{}(a) // compiles and its result is convertible to std::size_t template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // Constrained C++20 function template: template<Hashable T> void f(T) {} // // Alternative ways to apply the same constraint: // 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 satisfies Hashable // f(meow{}); // Error: meow does not satisfy Hashable }
制約の違反は、テンプレートのインスタンス化プロセスの初期段階でコンパイル時に検出されるため、理解しやすいエラーメッセージが表示されます。
std::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // Typical compiler diagnostic without concepts: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 lines of output ... // // Typical compiler diagnostic with concepts: // 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 | - | 任意の数の属性のシーケンス |
// concept template<class T, class U> concept Derived = std::is_base_of<U, T>::value;
コンセプトは再帰的に自身を参照することはできず、制約を受けることもできません。
template<typename T> concept V = V<T*>; // error: recursive concept template<class T> concept C1 = true; template<C1 T> concept Error1 = true; // Error: C1 T attempts to constrain a concept definition template<class T> requires C1<T> concept Error2 = true; // Error: the requires clause attempts to constrain a concept
コンセプトの明示的なインスタンス化、明示的な特殊化、または部分特殊化は許可されません (制約の元の定義の意味を変更することはできません)。
コンセプトは id-expression で名前付けできます。id-expression の値は、制約式が満たされる場合は true、そうでない場合は false です。
コンセプトは、次の一部として、型制約でも名前付けできます。
型制約では、コンセプトの最初の引数として文脈的に推論された型が暗黙的に使用されるため、コンセプトはパラメーターリストが要求するよりも 1 つ少ないテンプレート引数を取ります。
template<class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T is constrained by Derived<T, Base>
[編集] 制約
制約は、テンプレート引数に対する要件を指定する論理演算とオペランドのシーケンスです。requires 式内、またはコンセプトの本体として直接現れることができます。
制約には3種類(C++26まで)4種類(C++26以降)あります。
|
4) 畳み込み展開された制約
|
(C++26以降) |
宣言に関連付けられた制約は、オペランドが次の順序で並んだ論理 AND 式を正規化することによって決定されます。
- 制約された型テンプレートパラメータ、または制約されたプレースホルダー型で宣言された非型テンプレートパラメータそれぞれに対して導入された制約式 (出現順);
- テンプレートパラメータリスト後のrequires 句内の制約式;
- 省略された関数テンプレート宣言において、制約されたプレースホルダー型を持つ各パラメータに対して導入された制約式;
- 後続のrequires 句内の制約式。
この順序は、充足性をチェックする際に制約がインスタンス化される順序を決定します。
[編集] 再宣言
制約付き宣言は、同じ構文形式を用いてのみ再宣言できます。診断は不要です。
// These first two declarations of f are fine template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // OK, redeclaration // Inclusion of this third, logically-equivalent-but-syntactically-different // declaration of f is ill-formed, no diagnostic required template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // The following two declarations have different constraints: // the first declaration has Incrementable<T> && Decrementable<T> // the second declaration has Decrementable<T> && Incrementable<T> // Even though they are logically equivalent. template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // ill-formed, no diagnostic required
[編集] 連言
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, calls #2. When checking the constraints of #1, // 'sizeof(char) > 1' is not satisfied, so get_value<T>() is not checked }
[編集] 選言
2つの制約の選言は、制約式で || 演算子を使用することによって形成されます。
2つの制約の選言は、いずれかの制約が満たされた場合に満たされます。選言は左から右に評価され、短絡評価されます (左側の制約が満たされた場合、右側の制約へのテンプレート引数置換は試行されません)。
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
[編集] アトミック制約
アトミック制約は、式 E と、E 内に現れるテンプレートパラメータから、制約されるエンティティのテンプレートパラメータを含むテンプレート引数へのマッピング (これを「パラメータマッピング」と呼びます) で構成されます。
アトミック制約は制約の正規化中に形成されます。E は決して論理 AND または論理 OR 式ではありません (それらはそれぞれ連言と選言を形成します)。
アトミック制約の充足性は、パラメータマッピングとテンプレート引数を式 E に代入することによってチェックされます。代入の結果が無効な型または式になる場合、制約は満たされません。それ以外の場合、E は、lvalue から rvalue への変換の後、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); // error: S<int>{} does not have type bool when checking #1, // even though #2 is a better match }
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); // error, ambiguous: // the is_meowable<T> in Meowable and BadMeowableCat forms distinct atomic // constraints that are not identical (and so do not subsume each other) f2(0); // OK, calls #4, more constrained than #3 // GoodMeowableCat got its is_meowable<T> from 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>; // in C++23, these two overloads of g() have distinct atomic constraints // that are not identical and so do not subsume each other: calls to g() are ambiguous // in C++26, the folds are expanded and constraint on overload #2 (both move and copy // required), subsumes constraint on overload #1 (just the move is required) 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を持つ式 C<A1, A2, ... , AN> の正規形は、Cの各アトミック制約のパラメータマッピングにおいて、A1,A2, ...,ANをCの対応するテンプレートパラメータに代入した後のCの制約式の正規形です。そのようなパラメータマッピングへの代入がいずれも無効な型または式になった場合、プログラムは不適格であり、診断は不要です。
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // OK: normalized to the disjunction of // - T::value (with mapping T -> U*) and // - true (with an empty mapping). // No invalid type in mapping even though // T::value is ill-formed for all pointer types template<typename V> concept C = B<V&>; // Normalizes to the disjunction of // - T::value (with mapping T-> V&*) and // - true (with an empty mapping). // Invalid type V&* formed in mapping => ill-formed NDR
|
(C++26以降) |
- 他の任意の式 E の正規形は、その式が E であり、パラメータマッピングが恒等マッピングであるアトミック制約です。これには、
&&または||演算子を畳み込むものも含め、すべての畳み込み式が含まれます。
&& または || のユーザー定義オーバーロードは、制約の正規化には影響しません。
[編集] requires 句
キーワード requires は、「requires 句」を導入するために使用され、テンプレート引数または関数宣言に対する制約を指定します。
template<typename T> void f(T&&) requires Eq<T>; // can appear as the last element of a function declarator template<typename T> requires Addable<T> // or right after a template parameter list 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>(); // error, is_purrable<T>() is not a primary expression 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 よりも「より部分順序制約されている」と言われます。
- 両者は同じパラメータ型リストを持ちます。ただし、明示的オブジェクトパラメータの型は除外します(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 subsumes Decrementable, but not the other way around template<Decrementable T> void f(T); // #1 template<RevIterator T> void f(T); // #2, more constrained than #1 f(0); // int only satisfies Decrementable, selects #1 f((int*)0); // int* satisfies both constraints, selects #2 as more constrained template<class T> void g(T); // #3 (unconstrained) template<Decrementable T> void g(T); // #4 g(true); // bool does not satisfy Decrementable, selects #3 g(0); // int satisfies Decrementable, selects #4 because it is more constrained 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); // ambiguous
[編集] 注釈
| 機能テストマクロ | 値 | 規格 | 機能 |
|---|---|---|---|
__cpp_concepts |
201907L |
(C++20) | 制約 |
202002L |
(C++20) | 条件付きで自明な特殊メンバー関数 |
[編集] キーワード
[編集] 欠陥報告
以下の動作変更を伴う欠陥報告が、以前に公開されたC++標準に遡って適用されました。
| DR | 適用対象 | 公開された動作 | 正しい動作 |
|---|---|---|---|
| CWG 2428 | C++20 | 属性をコンセプトに適用できなかった | 許可 |
[編集] 関連項目
| Requires 式(C++20) | 制約を記述する bool 型の prvalue 式を生成します |