制約とコンセプト (C++20以上)
- このページは C++20 に採用されているコア言語の機能を説明しています。 標準ライブラリの仕様で使用されている名前付き型要件については名前付き要件を参照してください。 この機能の Concept TS バージョンについてはここを参照してください。
クラステンプレート、関数テンプレート、および非テンプレート関数 (一般的にはクラステンプレートのメンバ) は、制約と紐付けることができます。 制約はテンプレート引数に対する制限を指定します。 これは最も適切な関数オーバーロードおよびテンプレート特殊化を選択するために使用することができます。
そのような要件の名前付き集合はコンセプトと呼ばれます。 それぞれのコンセプトは述語であり、コンパイル時に評価され、制約として使用されたテンプレートのインタフェースの一部になります。
#include <string>
#include <cstddef>
#include <concepts>
using namespace std::literals;
// コンセプト「Hashable」の宣言。
// T 型の値 a について、式 std::hash<T>{}(a) がコンパイルでき、その結果が
// std::size_t に変換可能であるような、任意の型 T によって満たされます。
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
struct meow {};
template<Hashable T>
void f(T); // 制約付き関数テンプレート。
// 同じ制約を適用する別の方法。
// template<typename T>
// requires Hashable<T>
// void f(T);
//
// template<typename T>
// void f(T) requires Hashable<T>;
int main() {
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行ほどの出力 ...
//
//コンセプトのある一般的なコンパイラの診断:
// error: cannot call std::sort with std::_List_iterator<int>
// note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
コンセプトの意図は、構文的な制限 (加算演算子を持つ、配列である、など) ではなく、意味的なカテゴリ (数値、範囲、普通の関数、など) をモデル化することです。 ISO C++ core guideline T.20 によれば、「意味のあるセマンティクスを指定する能力は、構文的な制約とは対照的な、真の概念を表現する特性です」。
コンセプト
コンセプトは要件の名前付き集合です。 コンセプトの定義は名前空間スコープに現れなければなりません。
コンセプトの定義は以下の形式を持ちます。
template < template-parameter-list >
|
|||||||||
// コンセプト。
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 節がコンセプトを制約しようとしています。
コンセプトの明示的実体化、明示的特殊化、部分特殊化はできません (制約の元の定義の意味は変更できません)。
コンセプトは識別子式で指定することができます。 制約式が満たされる場合は、識別子式の値は true、そうでなければ false です。 コンセプトは以下の一部として型制約で指定することもできます。
制約
制約はテンプレート引数に対する要件を満たす論理演算子と被演算子の並びです。 これらは requires 式 (後述) 内で、およびコンセプトの本体として直接、現れることができます。
3種類の制約があります。
宣言に紐付けられた制約は被演算子が以下の順序である論理積式を正規化することによって決定されます。
- それぞれの制約付きテンプレート引数に対して導入された制約式 (出現順)。
- テンプレート引数リストの後の requires 節の制約式。
- 後置 requires 節の制約式。
この順序は満たすかどうかをチェックするときに制約が実体化される順序を決定します。
制約付き宣言は同じ構文形式を用いてのみ再宣言しても構いません。 診断は要求されません。
template<Incrementable T>
void f(T) requires Decrementable<T>;
template<Incrementable T>
void f(T) requires Decrementable<T>; // OK、再宣言。
template<typename T>
requires Incrementable<T> && Decrementable<T>
void f(T); // ill-formed (診断は要求されません)。
// 以下の2つの宣言は異なる制約を持ちます
// (たとえ論理的には同等であっても)。
// 1つめの宣言は 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>; // ill-formed (診断は要求されません)。
論理積
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 が論理積式または論理和式であることはありません (これらはそれぞれ論理積および論理和を形成します)。
原子制約を満たすかどうかは、引数マッピングとテンプレート実引数を式 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 がより良いマッチであったとしても。
}
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、 #3 より多く制約されている #4 を呼びます。
// GoodMeowableCat は Meowable から is_meowable<T> を得ています。
}
制約の正規化
制約の正規化は制約式を原子制約の論理積と論理和の並びに変換する処理です。 式の正規形は以下のように定義されます。
- 式
(E)の正規形はEの正規形です。 - 式
E1 && E2の正規形はE1とE2の正規形の論理積です。 - 式
E1 || E2の正規形はE1とE2の正規形の論理和です。 - 式
C<A1, A2, ... , AN>(Cがコンセプトを表す場合) の正規形は、Cのそれぞれのテンプレート引数について、 C のそれぞれの原子制約の引数マッピングで A1, A2, ... , AN を置き換えた後の、Cの制約式の正規形です。 引数マッピングへのそのような置き換えのいずれかの結果が無効な型または式となった場合は、プログラムは ill-formed です (診断は要求されません)。
template<typename T> concept A = T::value || true;
template<typename U>
concept B = A<U*>; // OK、以下の論理和に正規化されます。
// ・T::value (T → U* のマッピングを持ちます)
// ・true (空のマッピングを持ちます)
// たとえすべてのポインタ型について T::value が ill-formed であっても、
// マッピング内に無効な型はありません。
template<typename V>
concept C = B<V&>; // 以下の論理和に正規化されます。
// ・T::value (T → V&* のマッピングを持ちます)
// ・true (空のマッピングを持ちます)
// マッピング内に形成された無効な型 V&* → ill-formed (診断は要求されません)。
- それ以外のあらゆる式
Eの正規形は、式がEであり引数マッピングが恒等マッピングである原子制約です。 これにはすべての畳み込み式が含まれます (たとえ&&または||演算子の畳み込みであっても)。
&& または || のユーザ定義オーバーロードは制約の正規化において効果を持ちません。
requires 節
キーワード 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 式
キーワード requires は requires 式を開始するために使用することもできます。 これはいくつかのテンプレート引数に対する制約を記述する bool 型の prvalue 式です。 そのような式は、制約が満たされる場合は true、そうでなければ false です。
template<typename T>
concept Addable = requires (T x) { x + x; }; // requires 式。
template<typename T> requires Addable<T> // requires 節 (requires 式ではありません)。
T add(T a, T b) { return a + b; }
template<typename T>
requires requires (T x) { x + x; } // アドホック制約 (キーワードが2回使用されていることに注意してください)。
T add(T a, T b) { return a + b; }
requires 式の構文は以下の通りです。
requires { requirement-seq }
|
|||||||||
requires ( parameter-list(オプション) ) { requirement-seq }
|
|||||||||
| parameter-list | - | 関数宣言と同様の引数のコンマ区切りのリスト。 ただしデフォルト引数は使用できず、省略記号 (パック展開を表すもの以外) で終わることはできません。 これらの引数は、記憶域、リンケージ、生存期間を持たず、要件の指定を補助するためにのみ使用されます。 これらの引数は requirement-seq の閉じ括弧 } までがスコープ内です。
|
| requirement-seq | - | 要件の並び。 下で説明されます (それぞれの要件はセミコロンで終わります)。 |
requirements-seq 内のそれぞれの要件は以下のいずれかです。
- 単純要件。
- 型要件。
- 複合要件。
- ネストした要件。
要件はスコープ内のテンプレート引数、 parameter-list で導入されたローカルな引数、および囲っている文脈から可視なその他の任意の宣言を参照しても構いません。
テンプレート化されたエンティティの宣言で使用される requires 式内へのテンプレート引数の置き換えの結果、その要件内で無効な型や式を形成したり、それらの要件の意味制約の違反となることがあります。 そのような場合、 requires 式は false に評価され、プログラムを ill-formed にはしません。 置き換えと意味制約のチェックは字句順に進行し、 requires 式の結果を決定する条件に遭遇したときに停止します。 置き換え (もしあれば) および意味制約のチェックが成功した場合、 requires 式は true に評価されます。
すべての有り得るテンプレート引数について requires 式で置き換えの失敗が発生するであろう場合、プログラムは ill-formed です (診断は要求されません)。
template<class T> concept C = requires {
new int[-(int)sizeof(T)]; // あらゆる T に対して無効。 ill-formed です (診断は要求されません)。
};
requires 式がその要件に無効な型または式を含み、テンプレート化されたエンティティ内にそれが現れない場合、プログラムは ill-formed です。
単純要件
単純要件は任意の式文です。 その式が有効であることを表明します。 式は未評価被演算子です。 言語の正しさのみがチェックされます。
template<typename T>
concept Addable =
requires (T a, T b) {
a + b; // 「式 a + b はコンパイルできる有効な式である」
};
template <class T, class U = T>
concept 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 C =
requires {
typename T::inner; // 要求されるネストしたメンバの名前。
typename S<T>; // 要求されるクラステンプレートの特殊化。
typename Ref<T>; // 要求されるエイリアステンプレートの置き換え。
};
template <class T, class U> using CommonType = std::common_type_t<T, U>;
template <class T, class U> concept 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)} };
};
複合要件
複合要件は以下の形式を持ち、指定された式の性質を表明します。
{ expression } noexcept(オプション) return-type-requirement(オプション) ;
|
|||||||||
| return-type-requirement | - | -> type-constraint
|
置き換えおよび意味制約のチェックは以下の順序で進行します。
decltype((expression)) は type-constraint によって課される制約を満たさなければなりません。 そうでなければ、囲っている requires 式は false になります。template<typename T> concept C2 =
requires(T x) {
{*x} -> std::convertible_to<typename T::inner>; // 式 *x が有効でなければならず、
// さらに、型 T::inner が有効でなければならず、
// さらに、 *x の結果が T::inner に変換可能でなければなりません。
{x + 1} -> std::same_as<int>; // 式 X + 1 が有効でなければならず、
// さらに、 std::Same<decltype((x + 1)), int> が満たされなければなりません。
// すなわち、 (x + 1) は int 型の prvalue でなければなりません。
{x * 1} -> std::convertible_to<T>; // 式 x * 1 が有効でなければならず、
// さらに、その結果が T に変換可能でなければなりません。
};
ネストした要件
ネストした要件は以下の形式を持ちます。
requires constraint-expression ;
|
|||||||||
これはローカルな引数を用いた追加の制約を指定するために使用できます。 constraint-expression は置き換えられたテンプレート引数によって満たされなければなりません。 ネストした要件へのテンプレート引数の置き換えは、 constraint-expression が満たされるかどうかを決定するために必要とされる範囲にのみ constraint-expression への置き換えを発生させます。
template <class T>
concept 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] }; // 複合要件
};
制約の半順序
いかなるそれ以上の解析よりも前に、残っているものが原子制約に対する論理積と論理和の並びになるまで、すべての名前コンセプトおよびすべての requires 式の本体を置き換えることによって、制約が正規化されます。
P および Q 内の原子制約の同一性に至るまで P が Q を含むことが証明できる場合、制約 P は制約 Q を包含すると言います (型および式は同等性について解析されません。 N > 0 は N >= 0 を包含しません)。
具体的には、まず P が論理和正規形に変換され、 Q が論理積正規形に変換されます。 以下の場合に限り、 P は Q を包含します。
Pの論理和正規形のすべての論理和節が、Qの論理積正規形のすべての論理積節を包含する。 ただし、UがVを包含するような論理和節内の原子制約Uおよび論理積節内の原子制約Vが存在する場合に限り、論理和節は論理積節を包含します。- 上で説明しているルールを用いて同一である場合に限り、原子制約
Aは原子制約Bを包含します。
包含関係は制約の半順序を定義します。 これは以下のものを決定するために使用されます。
- オーバーロード解決における非テンプレート関数に対する最適候補。
- オーバーロード集合内の非テンプレート関数のアドレス。
- テンプレートテンプレート引数に対するベストマッチ。
- クラステンプレートの特殊化の半順序。
- 関数テンプレートの半順序。
| This section is incomplete Reason: backlinks from the above to here |
宣言 D1 および D2 が制約付きであり、 D1 に紐付く制約が D2 に紐付く制約を包含する (または D2 が制約付きでない) 場合、 D1 は D2 と少なくとも同程度に制約されていると言います。 D1 が D2 と少なくとも同程度に制約されており、 D2 が D1 と少なくとも同程度に制約されていない場合、 D1 は D2 より多く制約されています。
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); // 曖昧です。