Constraints and concepts
このページは実験的なコア言語機能について説明しています。標準ライブラリの仕様で使用される名前付き型要件については、 名前付き要件 を参照してください
クラステンプレート 、 関数テンプレート 、および非テンプレート関数(通常はクラステンプレートのメンバー)は、 制約 に関連付けることができます。これはテンプレート引数に対する要件を指定し、最も適切な関数オーバーロードとテンプレート特殊化を選択するために使用できます。
制約は、指定された要件を満たす型のみに変数宣言や関数戻り値型の自動型推論を制限するためにも使用できます。
このような要件の名前付きセットは concepts と呼ばれます。各コンセプトはコンパイル時に評価される述語であり、制約として使用されるテンプレートのインターフェースの一部となります:
#include <string> #include <locale> using namespace std::literals; // コンセプト "EqualityComparable" の宣言 // 型Tがこのコンセプトを満たすのは、型Tの値aとbに対して // 式 a==b がコンパイルされ、その結果がboolに変換可能な場合 template<typename T> concept bool EqualityComparable = requires(T a, T b) { { a == b } -> bool; }; void f(EqualityComparable&&); // 制約付き関数テンプレートの宣言 // template<typename T> // void f(T&&) requires EqualityComparable<T>; // 同じものの長い形式 int main() { f("abc"s); // OK, std::stringはEqualityComparableを満たす f(std::use_facet<std::ctype<char>>(std::locale{})); // エラー: EqualityComparableを満たさない }
制約違反はコンパイル時に検出され、テンプレートのインスタンス化プロセスの早い段階で行われるため、理解しやすいエラーメッセージが得られます。
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行の出力 ... // //コンセプトありの場合の典型的なコンパイラ診断メッセージ: // 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 によれば、「意味のあるセマンティクスを指定できる能力は、構文的制約とは対照的に、真のコンセプトを定義する特徴です。」
機能テストがサポートされている場合、ここで説明する機能はマクロ定数 __cpp_concepts によって示され、その値は 201507 以上となります。
目次 |
プレースホルダー
制約のないプレースホルダー
auto
および
制約付きプレースホルダー
は、推論される型のプレースホルダーです。制約付きプレースホルダーは
concept-name
<
template-argument-list
(optional)
>
の形式を持ちます。
プレースホルダーは、変数宣言(その場合、初期化子から推論される)または関数の戻り値の型(その場合、return文から推論される)に現れることがあります。
std::pair<auto, auto> p2 = std::make_pair(0, 'a'); // 最初のautoはint型、 // 2番目のautoはchar型 Sortable x = f(y); // xの型はfの戻り値の型から推論される、 // 型が制約Sortableを満たす場合のみコンパイル可能 auto f(Container) -> Sortable; // 戻り値の型はreturn文から推論される // 型がSortableを満たす場合のみコンパイル可能
プレースホルダーはパラメータ内にも現れることがあり、その場合、それらは関数宣言をテンプレート宣言に変換します(プレースホルダーが制約付きである場合は制約付きテンプレート宣言となります)
void f(std::pair<auto, EqualityComparable>); // これは2つのパラメータを持つテンプレートです: // 制約のない型パラメータと制約付きの非型パラメータ
制約付きプレースホルダーは auto が使用可能な任意の場所で使用できます。例えば、ジェネリックラムダ宣言などで使用されます。
auto gl = [](Assignable& a, auto* b) { a = *b; };
制約付き型指定子が非型またはテンプレートを指定しているが、制約付きプレースホルダーとして使用された場合、プログラムは不適格です:
template<size_t N> concept bool Even = (N%2 == 0); struct S1 { int n; }; int Even::* p2 = &S1::n; // エラー:非型コンセプトの不正な使用 void f(std::array<auto, Even>); // エラー:非型コンセプトの不正な使用 template<Even N> void f(std::array<auto, N>); // OK
省略テンプレート
関数のパラメータリストに1つ以上のプレースホルダーが現れる場合、その関数宣言は実際には関数テンプレート宣言であり、そのテンプレートパラメータリストには、出現順に、すべてのユニークなプレースホルダーに対して発明されたパラメータが含まれます
// 短縮形式 void g1(const EqualityComparable*, Incrementable&); // 完全形式: // template<EqualityComparable T, Incrementable U> void g1(const T*, U&); // より詳細な形式: // template<typename T, typename U> // void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>; void f2(std::vector<auto*>...); // 完全形式: template<typename... T> void f2(std::vector<T*>...); void f4(auto (auto::*)(auto)); // 完全形式: template<typename T, typename U, typename V> void f4(T (U::*)(V));
同等の制約付き型指定子によって導入されるすべてのプレースホルダーは、同じ発明されたテンプレートパラメータを持ちます。しかし、各非制約指定子(
auto
)は常に異なるテンプレートパラメータを導入します
void f0(Comparable a, Comparable* b); // 長い形式: template<Comparable T> void f0(T a, T* b); void f1(auto a, auto* b); // 長い形式: template<typename T, typename U> f1(T a, U* b);
関数テンプレートとクラステンプレートの両方は、
template introduction
を使用して宣言することができます。その構文は
concept-name
{
parameter-list
(optional)
}
であり、この場合
template
キーワードは必要ありません:template introductionの
parameter-list
の各パラメータは、名前付けられたconcept内の対応するパラメータの種類によって決定される種類(型、非型、テンプレート)のテンプレートパラメータになります。
テンプレートを宣言することに加えて、テンプレート導入は、 述語制約 (下記参照)を関連付けます。この制約は、導入によって命名された概念を(変数コンセプトの場合)命名するか、または(関数コンセプトの場合)呼び出します。
EqualityComparable{T} class Foo; // 長い形式: template<EqualityComparable T> class Foo; // より長い形式: template<typename T> requires EqualityComparable<T> class Foo; template<typename T, int N, typename... Xs> concept bool Example = ...; Example{A, B, ...C} struct S1; // 長い形式 template<class A, int B, class... C> requires Example<A,B,C...> struct S1;
関数テンプレートでは、テンプレート導入をプレースホルダーと組み合わせることができます:
Sortable{T} void f(T, auto); // 長い形式: template<Sortable T, typename U> void f(T, U); // プレースホルダーのみを使用した代替形式: void f(Sortable, auto);
|
このセクションは不完全です
理由: テンプレート宣言ページを調整し、ここにリンクを追加する必要があります |
コンセプト
コンセプトは名前付き要件の集合である。コンセプトの定義は名前空間スコープで現れ、 関数テンプレート 定義の形式を取る場合(この場合 関数コンセプト と呼ばれる)または 変数テンプレート 定義の形式を取る場合(この場合 変数コンセプト と呼ばれる)。唯一の違いは、キーワード concept が decl-specifier-seq に現れることである:
// 標準ライブラリからの変数コンセプト (Ranges TS) template <class T, class U> concept bool Derived = std::is_base_of<U, T>::value; // 標準ライブラリからの関数コンセプト (Ranges TS) template <class T> concept bool EqualityComparable() { return requires(T a, T b) { {a == b} -> Boolean; {a != b} -> Boolean; }; }
関数コンセプトには以下の制限が適用されます:
-
inlineおよびconstexprは許可されません。関数は自動的にinlineおよびconstexprとなります -
friendおよびvirtualは許可されません -
例外指定は許可されません。関数は自動的に
noexcept(true)となります - 後で宣言と定義を分離できず、再宣言もできません
-
戻り値の型は
boolでなければなりません - 戻り値型の推論は許可されません
- パラメータリストは空でなければなりません
-
関数本体は
return文のみで構成され、その引数は 制約式 (述語制約、他の制約の論理積/論理和、または後述のrequires式)でなければなりません
変数コンセプトには以下の制限が適用されます:
-
型は
boolでなければならない - 初期化子なしで宣言することはできない
- クラススコープで宣言することはできない
-
constexprは許可されず、変数は自動的にconstexprとなる - 初期化子は制約式(述語制約、制約の論理積/論理和、または後述のrequires式)でなければならない
コンセプトは、関数本体内または変数の初期化子内で自身を再帰的に参照することはできません:
template<typename T> concept bool F() { return F<typename T::type>(); } // エラー template<typename T> concept bool V = V<T*>; // エラー
概念の明示的なインスタンス化、明示的特殊化、または部分特殊化は許可されない(制約の元の定義の意味を変更することはできない)
制約条件
制約は、テンプレート引数に対する要件を指定する一連の論理演算のシーケンスです。これらは requires-expression 内(下記参照)およびコンセプトの本体として直接記述できます
制約には9つの種類があります:
最初の3種類の制約は、コンセプトの本体として直接、またはアドホックなrequires節として現れることがあります:
template<typename T> requires // requires節(アドホック制約) sizeof(T) > 1 && get_value<T>() // 2つの述語制約の論理積 void f(T);
同じ宣言に複数の制約が付与されている場合、全体の制約は以下の順序での論理積となります: template introduction によって導入される制約、各テンプレートパラメータに対する制約(出現順)、テンプレートパラメータリスト後の requires 節、各関数パラメータに対する制約(出現順)、末尾の requires 節:
// これらの宣言は同じ制約付き関数テンプレートを宣言している // 制約は Incrementable<T> && Decrementable<T> template<Incrementable T> void f(T) requires Decrementable<T>; template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // ok // 以下の2つの宣言は異なる制約を持つ: // 最初の宣言は Incrementable<T> && Decrementable<T> // 2番目の宣言は Decrementable<T> && Incrementable<T> // 論理的には等価であるにもかかわらず // 2番目の宣言は不適格であり、診断は要求されない template<Incrementable T> requires Decrementable<T> void g(); template<Decrementable T> requires Incrementable<T> void g(); // error
接続詞
制約の論理積
P
と
Q
は
P
&&
Q
として指定されます。
// 標準ライブラリからのコンセプト例 (Ranges TS) template <class T> concept bool Integral = std::is_integral<T>::value; template <class T> concept bool SignedIntegral = Integral<T> && std::is_signed<T>::value; template <class T> concept bool UnsignedIntegral = Integral<T> && !SignedIntegral<T>;
2つの制約の論理積は、両方の制約が満たされた場合にのみ満たされます。論理積は左から右へ評価され、短絡評価されます(左側の制約が満たされない場合、右側の制約へのテンプレート引数の代入は試みられません:これにより、即時コンテキスト外での代入による失敗を防ぎます)。制約の論理積においてユーザー定義の
operator&&
のオーバーロードは許可されていません。
論理和
制約の論理和
P
と
Q
は
P
||
Q
として指定されます。
2つの制約の論理和は、いずれかの制約が満たされた場合に満たされます。論理和は左から右へ評価され、短絡評価されます(左側の制約が満たされた場合、右側の制約へのテンプレート引数推論は試みられません)。制約の論理和では、
operator||
のユーザー定義オーバーロードは許可されません。
// 標準ライブラリからの制約例 (Ranges TS) template <class T = void> requires EqualityComparable<T>() || Same<T, void> struct equal_to;
述語制約
述語制約は bool 型の定数式です。これは true と評価された場合にのみ満たされます。
template<typename T> concept bool Size32 = sizeof(T) == 4;
述語制約は、非型テンプレートパラメータおよびテンプレートテンプレート引数に対する要件を指定できます。
述語制約は直接 bool に評価されなければならず、変換は許可されません:
template<typename T> struct S { constexpr explicit operator bool() const { return true; } }; template<typename T> requires S<T>{} // 不適切な述語制約: S<T>{} は bool 型ではない void f(T); f(0); // エラー: 制約が満たされない
要件
キーワード requires は2つの方法で使用されます:
template<typename T> void f(T&&) requires Eq<T>; // 関数宣言子の最後の要素として記述可能 template<typename T> requires Addable<T> // またはテンプレートパラメータリストの直後に記述可能 T add(T a, T b) { return a + b; }
true
となり、そうでない場合はfalseとなる:
template<typename T> concept bool Addable = requires (T x) { x + x; }; // requires-expression template<typename T> requires Addable<T> // requires-clause, not requires-expression T add(T a, T b) { return a + b; } template<typename T> requires requires (T x) { x + x; } // ad-hoc constraint, note keyword used twice T add(T a, T b) { return a + b; }
requires-expression の構文は以下の通りです:
requires
(
parameter-list
(optional)
)
{
requirement-seq
}
|
|||||||||
| parameter-list | - |
関数宣言と同様のカンマ区切りのパラメータリスト。ただし、デフォルト引数は許可されず、最後のパラメータは省略記号にできません。これらのパラメータにはストレージ、リンケージ、寿命がありません。これらのパラメータは
requirement-seq
の終了
}
までスコープ内にあります。パラメータが使用されない場合、丸括弧も省略できます
|
| requirement-seq | - | 以下で説明する(各要件はセミコロンで終わる) requirements の空白区切りシーケンス。各要件は、このrequires-expressionが定義する制約の 論理積 に別の制約を追加します。 |
各要件は requirements-seq において以下のいずれかです:
- 単純要件
- 型要件
- 複合要件
- ネスト要件
要件は、スコープ内にあるテンプレートパラメータと、 parameter-list で導入されたローカルパラメータを参照することができます。パラメータ化された場合、requires式は parametrized constraint を導入すると言われます。
テンプレート引数のrequires式への代入は、その要件内で無効な型や式の形成をもたらす可能性があります。このような場合、
- requires式が テンプレート化されたエンティティ の宣言外で使用される場合に置換失敗が発生すると、プログラムは不適格となる。
- requires式が テンプレート化されたエンティティ の宣言で使用される場合、対応する制約は「満たされていない」ものとして扱われ、 置換失敗はエラーではない 、ただし
- 全ての可能なテンプレート引数に対してrequires式で置換失敗が発生する場合、プログラムは不適格となり、診断は要求されない:
template<class T> concept bool C = requires { new int[-(int)sizeof(T)]; // すべてのTに対して無効:不正な形式、診断は不要 };
シンプルな要件
単純な要件は任意の式文です。この要件は式が有効であることです(これは 式制約 です)。述語制約とは異なり、評価は行われず、言語的な正しさのみがチェックされます。
template<typename T> concept bool Addable = requires (T a, T b) { a + b; // 「式 a+b がコンパイル可能な有効な式であること」 }; // 標準ライブラリからの制約例 (ranges TS) template <class T, class U = T> concept bool Swappable = requires(T&& t, U&& u) { swap(std::forward<T>(t), std::forward<U>(u)); swap(std::forward<U>(u), std::forward<T>(t)); };
型要件
型要件はキーワード typename に続けて、修飾された型名(任意)で構成されます。この要件は、指定された型が存在すること( 型制約 )を要求します:これは特定の名前付きネスト型が存在することを確認するため、またはクラステンプレートの特殊化が型を指すことを確認するため、あるいはエイリアステンプレートが型を指すことを確認するために使用できます。
template<typename T> using Ref = T&; template<typename T> concept bool C = requires { typename T::inner; // 必要なネストされたメンバ名 typename S<T>; // 必要なクラステンプレートの特殊化 typename Ref<T>; // 必要なエイリアステンプレートの置換 }; // 標準ライブラリからのコンセプト例 (Ranges TS) template <class T, class U> using CommonType = std::common_type_t<T, U>; template <class T, class U> concept bool Common = requires (T t, U u) { typename CommonType<T, U>; // CommonType<T, U> が有効で型を指す { CommonType<T, U>{std::forward<T>(t)} }; { CommonType<T, U>{std::forward<U>(u)} }; };
複合要件
複合要件は以下の形式を持ちます
{
式
}
noexcept
(オプション)
末尾戻り値型
(オプション)
;
|
|||||||||
以下の制約の論理積を指定します:
noexcept
が使用される場合、式もnoexceptでなければならない(
例外制約
)
template<typename T> concept bool C2 = requires(T x) { {*x} -> typename T::inner; // 式 *x は有効でなければならない // かつ型 T::inner は有効でなければならない // かつ *x の結果は T::inner に変換可能でなければならない }; // 標準ライブラリからの概念例 (Ranges TS) template <class T, class U> concept bool Same = std::is_same<T,U>::value; template <class B> concept bool Boolean = requires(B b1, B b2) { { bool(b1) }; // 直接初期化制約は式を使用しなければならない { !b1 } -> bool; // 複合制約 requires Same<decltype(b1 && b2), bool>; // ネストされた制約、下記参照 requires Same<decltype(b1 || b2), bool>; };
ネストされた要件
ネストされた要件は、セミコロンで終了する別の requires-clause です。これはローカルパラメータに適用された他の名前付きコンセプトを用いて表現される predicate constraints (上記参照)を導入するために使用されます(requires節の外側では、predicate constraintsはパラメータを使用できず、式を直接requires節に配置するとexpression constraintとなり、評価されないことを意味します)
// Ranges TSからの制約例 template <class T> concept bool Semiregular = DefaultConstructible<T> && CopyConstructible<T> && Destructible<T> && CopyAssignable<T> && requires(T a, size_t n) { requires Same<T*, decltype(&a)>; // ネスト要件: "Same<...>がtrueと評価される" { a.~T() } noexcept; // 複合要件: "a.~T()"が例外を投げない有効な式である requires Same<T*, decltype(new T)>; // ネスト要件: "Same<...>がtrueと評価される" requires Same<T*, decltype(new T[n])>; // ネスト要件 { delete new T }; // 複合要件 { delete new T[n] }; // 複合要件 };
Concept resolution
他の関数テンプレートと同様に、関数コンセプト(ただし変数コンセプトは除く)はオーバーロード可能です:同じ concept-name を使用する複数のコンセプト定義を提供することができます。
コンセプト解決は、 concept-name (修飾されている可能性あり)が以下の箇所に現れたときに実行されます
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 void f(C); // 概念Cによって参照される概念の集合は#1と#2の両方を含む; // 概念解決(下記参照)は#1を選択する。
概念解決を実行するために、 template parameters の各概念は、名前(および修飾がある場合は修飾)に一致するものに対して、 concept arguments のシーケンスと照合されます。これらはテンプレート引数と wildcards で構成されます。ワイルドカードはあらゆる種類(型、非型、テンプレート)のテンプレートパラメータに一致することができます。引数セットは、コンテキストに応じて異なる方法で構築されます
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f1(const C1*); // <wildcard> matches <T>, selects #1
template<typename T> concept bool C1() { return true; } // #1 template<typename T, typename U> concept bool C1() { return true; } // #2 void f2(C1<char>); // <wildcard, char> matches <T, U>, selects #2
template<typename... Ts> concept bool C3 = true; C3{T} void q2(); // OK: <T> matches <...Ts> C3{...Ts} void q1(); // OK: <...Ts> matches <...Ts>
template<typename T> concept bool C() { return true; } // #1 template<typename T, typename U> concept bool C() { return true; } // #2 template <typename T> void f(T) requires C<T>(); // matches #1
コンセプト解決は、各引数を可視コンセプトの対応するパラメータと照合することで実行されます。デフォルトテンプレート引数(使用されている場合)は、引数に対応しない各パラメータに対してインスタンス化され、引数リストに追加されます。テンプレートパラメータは、引数がワイルドカードでない限り、同じ種類(型、非型、テンプレート)を持つ場合にのみ引数と一致します。パラメータパックは、すべての引数が(ワイルドカードでない限り)種類においてパターンに一致する限り、ゼロ個以上の引数と一致します。
いずれかの引数が対応するパラメータと一致しない場合、または引数の数がパラメータより多く、最後のパラメータがパックでない場合、そのコンセプトは実現不可能です。実現可能なコンセプトがゼロまたは複数存在する場合、プログラムは不適格となります。
template<typename T> concept bool C2() { return true; } template<int T> concept bool C2() { return true; } template<C2<0> T> struct S1; // エラー: <ワイルドカード, 0> は // <typename T> にも <int T> にも一致しない template<C2 T> struct S2; // #1 と #2 の両方が一致: エラー
|
このセクションは不完全です
理由:意味のあるコンセプトの例が必要です。これらの 'return true' プレースホルダーでは不十分です |
制約の部分順序付け
さらなる分析の前に、制約はすべての名前コンセプトとすべてのrequires式の本体を置換することで 正規化 され、残るものはアトミック制約(述語制約、式制約、型制約、暗黙変換制約、引数推定制約、例外制約)上の連言と選言のシーケンスとなります。
コンセプト
P
は、型や式の等価性を分析することなく(したがって
N >= 0
は
N > 0
を包含しない)
P
が
Q
を
含意する
ことが証明できる場合、コンセプト
Q
を
包含する
と言われる
具体的には、まず
P
が選言標準形に変換され、
Q
が連言標準形に変換されます。その後、以下のように比較されます:
-
各アトミック制約
Aは等価なアトミック制約Aを包含する -
各アトミック制約
Aは論理和A||Bを包含するが、論理積A&&Bは包含しない -
各論理積
A&&BはAを包含するが、論理和A||BはAを包含しない
包含関係は制約の半順序を定義し、これは以下を決定するために使用されます:
- 非テンプレート関数に対する最適な実行可能候補( オーバーロード解決 )
- オーバーロード集合における 非テンプレート関数のアドレス
- テンプレートテンプレート引数に対する最適なマッチ
- クラステンプレート特殊化の部分順序付け
- 関数テンプレートの部分順序付け
|
このセクションは不完全です
理由:上記からのバックリンク |
もし宣言
D1
と
D2
が制約付きであり、D1の正規化された制約がD2の正規化された制約を包含する場合(あるいはD1が制約付きでD2が制約なしの場合)、D1はD2に対して
少なくとも同等に制約されている
と言います。D1がD2に対して少なくとも同等に制約されており、かつD2がD1に対して少なくとも同等に制約されていない場合、D1はD2よりも
より強く制約されている
と言います。
template<typename T> concept bool Decrementable = requires(T t) { --t; }; template<typename T> concept bool RevIterator = Decrementable<T> && requires(T t) { *t; }; // RevIteratorはDecrementableを包含するが、逆は成り立たない // RevIteratorはDecrementableよりも制約が強い void f(Decrementable); // #1 void f(RevIterator); // #2 f(0); // intはDecrementableのみを満たすため、#1を選択 f((int*)0); // int*は両方の制約を満たすため、より制約の強い#2を選択 void g(auto); // #3 (制約なし) void g(Decrementable); // #4 g(true); // boolはDecrementableを満たさないため、#3を選択 g(0); // intはDecrementableを満たすため、より制約の強い#4を選択
キーワード
コンパイラサポート
GCC >= 6.1 はこの技術仕様をサポートしています(必要なオプション - fconcepts )