制約とコンセプト
このページでは、実験的なコア言語機能について説明します。標準ライブラリの仕様で使用される名前付き型要件については、名前付き要件を参照してください。
クラステンプレート、関数テンプレート、および非テンプレート関数(通常はクラステンプレートのメンバ)は、制約に関連付けることができます。制約は、テンプレート引数に対する要件を指定し、最も適切な関数オーバーロードやテンプレート特殊化を選択するために使用できます。
制約は、変数宣言や関数戻り値の型の自動型推論を、指定された要件を満たす型のみに限定するためにも使用できます。
このような要件のまとまったセットはコンセプトと呼ばれます。各コンセプトはコンパイル時に評価される述語であり、制約として使用されるテンプレートのインターフェースの一部となります。
#include <string> #include <locale> using namespace std::literals; // Declaration of the concept "EqualityComparable", which is satisfied by // any type T such that for values a and b of type T, // the expression a==b compiles and its result is convertible to bool template<typename T> concept bool EqualityComparable = requires(T a, T b) { { a == b } -> bool; }; void f(EqualityComparable&&); // declaration of a constrained function template // template<typename T> // void f(T&&) requires EqualityComparable<T>; // long form of the same int main() { f("abc"s); // OK, std::string is EqualityComparable f(std::use_facet<std::ctype<char>>(std::locale{})); // Error: not EqualityComparable }
制約違反は、テンプレートのインスタンス化プロセスの早い段階であるコンパイル時に検出されるため、理解しやすいエラーメッセージにつながります。
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 によると、「意味のある意味論を指定できることは、構文的な制約とは対照的に、真のコンセプトの定義的な特徴です。」
機能テストがサポートされている場合、ここで説明されている機能はマクロ定数 __cpp_concepts によって示され、値は 201507 以上です。
目次 |
[編集] プレースホルダ
制約のないプレースホルダ auto と、concept-name < template-argument-list(optional)> の形式の制約付きプレースホルダは、推論される型のためのプレースホルダです。
プレースホルダは、変数宣言(この場合、初期化子から推論されます)または関数戻り値の型(この場合、return文から推論されます)に出現する可能性があります。
std::pair<auto, auto> p2 = std::make_pair(0, 'a'); // first auto is int, // second auto is char Sortable x = f(y); // the type of x is deduced from the return type of f, // only compiles if the type satisfies the constraint Sortable auto f(Container) -> Sortable; // return type is deduced from the return statement // only compiles if the type satisfies Sortable
プレースホルダはパラメータにも出現する可能性があり、この場合、関数宣言はテンプレート宣言に変わり(プレースホルダが制約付きであれば制約付きになります)。
void f(std::pair<auto, EqualityComparable>); // this is a template with two parameters: // unconstrained type parameter and a constrained non-type parameter
制約付きプレースホルダは、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; // error, invalid use of a non-type concept void f(std::array<auto, Even>); // error, invalid use of a non-type concept template<Even N> void f(std::array<auto, N>); // OK
[編集] 省略形テンプレート
1つ以上のプレースホルダが関数パラメータリストに出現する場合、関数宣言は実際には関数テンプレート宣言であり、そのテンプレートパラメータリストには、出現順に、各一意のプレースホルダに対応する1つのinventedパラメータが含まれます。
// short form void g1(const EqualityComparable*, Incrementable&); // long form: // template<EqualityComparable T, Incrementable U> void g1(const T*, U&); // longer form: // template<typename T, typename U> // void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>; void f2(std::vector<auto*>...); // long form: template<typename... T> void f2(std::vector<T*>...); void f4(auto (auto::*)(auto)); // long form: template<typename T, typename U, typename V> void f4(T (U::*)(V));
同等の制約付き型指定子によって導入されたすべてのプレースホルダは、同じinventedテンプレートパラメータを持ちます。ただし、各制約のない指定子(auto)は、常に異なるテンプレートパラメータを導入します。
void f0(Comparable a, Comparable* b); // long form: template<Comparable T> void f0(T a, T* b); void f1(auto a, auto* b); // long form: template<typename T, typename U> f1(T a, U* b);
関数テンプレートとクラステンプレートの両方を、concept-name { parameter-list(optional)} という構文を持つテンプレート導入を使用して宣言できます。この場合、キーワード template は不要です。テンプレート導入の parameter-list からの各パラメータは、対応する名前付きコンセプトのパラメータの種類(型、非型、テンプレート)によって決定される種類を持つテンプレートパラメータになります。
テンプレートを宣言するだけでなく、テンプレート導入は、導入によって名前が付けられたコンセプトに名前を付ける(変数コンセプトの場合)または呼び出す(関数コンセプトの場合)述語制約(下記参照)を関連付けます。
EqualityComparable{T} class Foo; // long form: template<EqualityComparable T> class Foo; // longer form: template<typename T> requires EqualityComparable<T> class Foo; template<typename T, int N, typename... Xs> concept bool Example = ...; Example{A, B, ...C} struct S1; // long form template<class A, int B, class... C> requires Example<A,B,C...> struct S1;
関数テンプレートの場合、テンプレート導入はプレースホルダと組み合わせて使用できます。
Sortable{T} void f(T, auto); // long form: template<Sortable T, typename U> void f(T, U); // alternative using only placeholders: void f(Sortable, auto);
| このセクションは未完成です 理由: テンプレート宣言ページを編集してここにリンクさせる |
[編集] コンセプト
コンセプトは、要件の名前付きセットです。コンセプトの定義は名前空間スコープに出現し、関数テンプレートの定義(この場合、関数コンセプトと呼ばれます)または変数テンプレートの定義(この場合、変数コンセプトと呼ばれます)の形式をとります。唯一の違いは、キーワード concept が decl-specifier-seq に出現することです。
// variable concept from the standard library (Ranges TS) template <class T, class U> concept bool Derived = std::is_base_of<U, T>::value; // function concept from the standard library (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>(); } // error template<typename T> concept bool V = V<T*>; // error
コンセプトの明示的なインスタンス化、明示的な特殊化、または部分特殊化は許可されません(制約の元の定義の意味を変更することはできません)。
[編集] 制約
制約は、テンプレート引数に対する要件を指定する論理演算のシーケンスです。これらは、requires式(下記参照)内や、コンセプトの本体として直接出現させることができます。
9種類の制約があります。
最初の3つの種類の制約は、コンセプトの本体やアドホックなrequires節として直接出現させることができます。
template<typename T> requires // requires-clause (ad-hoc constraint) sizeof(T) > 1 && get_value<T>() // conjunction of two predicate constraints void f(T);
複数の制約が同じ宣言に関連付けられている場合、全体の制約は次の順序で論理積となります:テンプレート導入によって導入された制約、出現順の各テンプレートパラメータの制約、テンプレートパラメータリストの後のrequires節、出現順の各関数パラメータの制約、末尾のrequires節。
// the declarations declare the same constrained function template // with the constraint 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 // 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. // The second declaration is ill-formed, no diagnostic required template<Incrementable T> requires Decrementable<T> void g(); template<Decrementable T> requires Incrementable<T> void g(); // error
[編集] 論理積
制約 P と Q の論理積は P && Q として指定されます。
// example concepts from the standard library (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|| のオーバーロードは、制約の論理和では許可されません。
// example constraint from the standard library (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>{} // bad predicate constraint: S<T>{} is not bool void f(T); f(0); // error: constraint never satisfied
[編集] 要件
キーワード requires は2つの方法で使用されます。
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; }
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式の構文は次のとおりです。
requires ( parameter-list(optional) ) { requirement-seq } |
|||||||||
| parameter-list | - | 関数宣言のようなカンマ区切りのパラメータリストですが、デフォルト引数は許可されず、最後のパラメータは省略記号(...)にすることはできません。これらのパラメータはストレージ、リンク、または寿命を持ちません。これらのパラメータは、requirement-seq の閉じ } までスコープ内にあります。パラメータが使用されない場合、丸括弧も省略できます。 |
| requirement-seq | - | 要件(下記参照)の空白区切りのシーケンス。各要件はセミコロンで終了します。各要件は、このrequires式が定義する制約の論理積に別の制約を追加します。 |
requirements-seq 内の各要件は、次のいずれかです。
- 単純要件
- 型要件
- 複合要件
- ネストされた要件
要件は、スコープ内にあるテンプレートパラメータや、parameter-list で導入されたローカルパラメータを参照できます。パラメータ化された場合、requires式はパラメータ化制約を導入すると言われます。
テンプレート引数をrequires式に代入すると、その要件内で無効な型または式が生成される可能性があります。そのような場合、
- requires式での置換失敗がテンプレートエンティティ宣言の外部で使用される場合、プログラムは不正形式となります。
- requires式がテンプレートエンティティの宣言で使用される場合、対応する制約は「満たされない」と見なされ、置換失敗はエラーではありません。ただし、
- あらゆる可能なテンプレート引数に対してrequires式で置換失敗が発生する場合、プログラムは不正形式となります(診断は不要)。
template<class T> concept bool C = requires { new int[-(int)sizeof(T)]; // invalid for every T: ill-formed, no diagnostic required };
[編集] 単純要件
単純要件は、任意の式ステートメントです。要件は、式が有効であることです(これは式制約です)。述語制約とは異なり、評価は行われず、言語の正しさのみがチェックされます。
template<typename T> concept bool Addable = requires (T a, T b) { a + b; // "the expression a+b is a valid expression that will compile" }; // example constraint from the standard library (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; // required nested member name typename S<T>; // required class template specialization typename Ref<T>; // required alias template substitution }; //Example concept from the standard library (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> is valid and names a type { CommonType<T, U>{std::forward<T>(t)} }; { CommonType<T, U>{std::forward<U>(u)} }; };
[編集] 複合要件
複合要件の形式は次のとおりです。
{ expression } noexcept(optional) trailing-return-type(optional) ; |
|||||||||
および、次の制約の論理積を指定します。
noexcept が使用されている場合、式も noexcept でなければならない(例外制約)。template<typename T> concept bool C2 = requires(T x) { {*x} -> typename T::inner; // the expression *x must be valid // AND the type T::inner must be valid // AND the result of *x must be convertible to T::inner }; // Example concept from the standard library (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) }; // direct initialization constraint has to use expression { !b1 } -> bool; // compound constraint requires Same<decltype(b1 && b2), bool>; // nested constraint, see below requires Same<decltype(b1 || b2), bool>; };
[編集] ネストされた要件
ネストされた要件は、セミコロンで終了する別のrequires節です。これは、ローカルパラメータに関して(requires節の外では、述語制約はパラメータを使用できず、requires節に式を直接置くと式制約になり、評価されないことを意味します)表現される述語制約(上記参照)を導入するために使用されます。
// example constraint from 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)>; // nested: "Same<...> evaluates to true" { a.~T() } noexcept; // compound: "a.~T()" is a valid expression that doesn't throw requires Same<T*, decltype(new T)>; // nested: "Same<...> evaluates to true" requires Same<T*, decltype(new T[n])>; // nested { delete new T }; // compound { delete new T[n] }; // compound };
[編集] コンセプト解決
他のすべての関数テンプレートと同様に、関数コンセプト(変数コンセプトではない)はオーバーロードできます。同じ 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); // the set of concepts referred to by C includes both #1 and #2; // concept resolution (see below) selects #1.
コンセプト解決を実行するために、一致する名前(および修飾子、もしあれば)を持つ各コンセプトのテンプレートパラメータが、コンセプト引数のシーケンス(テンプレート引数とワイルドカード)と一致します。ワイルドカードは、任意の種類のテンプレートパラメータ(型、非型、テンプレート)に一致させることができます。引数セットは、コンテキストによって異なる方法で構築されます。
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
コンセプト解決は、各引数を各可視コンセプトの対応するパラメータと一致させることによって実行されます。デフォルトテンプレート引数(使用されている場合)は、引数に対応しない各パラメータに対してインスタンス化され、その後、引数リストに追加されます。テンプレートパラメータは、引数がワイルドカードである場合を除き、種類(型、非型、テンプレート)が同じである場合にのみ引数と一致します。パラメータパックは、引数が(ワイルドカードでない限り)種類でパターンに一致する限り、ゼロ個以上の引数と一致します。
いずれかの引数が対応するパラメータと一致しない場合、または引数がパラメータより多く、最後のパラメータがパックでない場合、そのコンセプトは有効ではありません。有効なコンセプトがゼロ個または1個より多い場合、プログラムは不正形式となります。
template<typename T> concept bool C2() { return true; } template<int T> concept bool C2() { return true; } template<C2<0> T> struct S1; // error: <wildcard, 0> matches // neither <typename T> nor <int T> template<C2 T> struct S2; // both #1 and #2 match: error
| このセクションは未完成です 理由: ここには、これらの「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 subsumes Decrementable, but not the other way around // RevIterator is more constrained as Decrementable void f(Decrementable); // #1 void f(RevIterator); // #2 f(0); // int only satisfies Decrementable, selects #1 f((int*)0); // int* satisfies both constraints, selects #2 as more constrained void g(auto); // #3 (unconstrained) void g(Decrementable); // #4 g(true); // bool does not satisfy Decrementable, selects #3 g(0); // int satisfies Decrementable, selects #4 because it is more constrained
[編集] キーワード
[編集] コンパイラサポート
GCC >= 6.1 はこの技術仕様をサポートしています(必要なオプション -fconcepts)。