名前空間
変種
操作

制約とコンセプト (C++20 以降)

From cppreference.com
< cpp‎ | language
 
 
C++言語
全般
フロー制御
条件実行文
if
繰り返し文 (ループ)
for
範囲for (C++11)
ジャンプ文
関数
関数宣言
ラムダ式
inline指定子
動的例外仕様 (C++17まで*)
noexcept指定子 (C++11)
例外
名前空間
指定子
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
記憶域期間指定子
初期化
代替表現
リテラル
ブーリアン - 整数 - 浮動小数点数
文字 - 文字列 - nullptr (C++11)
ユーザー定義 (C++11)
ユーティリティ
属性 (C++11)
typedef宣言
型エイリアス宣言 (C++11)
キャスト
メモリ確保
クラス
クラス固有の関数プロパティ
explicit (C++11)
static

特殊メンバ関数
テンプレート
その他
 
 
 
 

クラステンプレート関数テンプレート (汎用ラムダを含む)、およびその他のテンプレート化された関数 (通常はクラステンプレートのメンバー) は、テンプレート引数に対する要件を指定する「制約」と関連付けられる場合があります。この制約は、最も適切な関数オーバーロードやテンプレート特殊化を選択するために使用できます。

このような要件の命名されたセットを「コンセプト」と呼びます。各コンセプトはコンパイル時に評価される述語であり、制約として使用されるテンプレートのインターフェースの一部となります。

#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 < テンプレート引数リスト >

concept コンセプト名 属性 (オプション) = 制約式;

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以降)あります。

1) 連言
2) 選言
3) アトミック制約
4) 畳み込み展開された制約
(C++26以降)

宣言に関連付けられた制約は、オペランドが次の順序で並んだ論理 AND 式を正規化することによって決定されます。

  1. 制約された型テンプレートパラメータ、または制約されたプレースホルダー型で宣言された非型テンプレートパラメータそれぞれに対して導入された制約式 (出現順);
  2. テンプレートパラメータリスト後のrequires内の制約式;
  3. 省略された関数テンプレート宣言において、制約されたプレースホルダー型を持つ各パラメータに対して導入された制約式;
  4. 後続の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
}

畳み込み展開された制約

「畳み込み展開された制約」は、制約 C と畳み込み演算子 (&& または || のいずれか) から形成されます。畳み込み展開された制約はパック展開です。

パック展開パラメータの要素数を N とします。

  • パック展開が無効な場合 (例えば、異なるサイズのパックを展開する場合など)、畳み込み展開された制約は満たされません。
  • N0 の場合、畳み込み演算子が && であれば畳み込み展開された制約は満たされ、畳み込み演算子が || であれば満たされません。
  • N が正の畳み込み展開された制約の場合、[1N] の各 i について、各パック展開パラメータは対応する i 番目の要素に昇順で置換されます。
  • 畳み込み演算子が && である畳み込み展開された制約の場合、j 番目の要素の置換が C に違反する場合、畳み込み展開された制約は満たされません。この場合、j より大きい i に対して置換は行われません。そうでない場合、畳み込み展開された制約は満たされます。
  • 畳み込み演算子が || である畳み込み展開された制約の場合、j 番目の要素の置換が C を満たす場合、畳み込み展開された制約は満たされます。この場合、j より大きい i に対して置換は行われません。そうでない場合、畳み込み展開された制約は満たされません。


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, ..., ANC の対応するテンプレートパラメータに代入した後の 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
  • (E && ...) および (... && E) の正規形は、CE の正規形であり、畳み込み演算子が && である畳み込み展開された制約です。
  • (E || ...) および (... || E) の正規形は、CE の正規形であり、畳み込み演算子が || である畳み込み展開された制約です。
  • (E1 && ... && E2) および (E1 || ... || E2) の正規形は、
  • (E1 && ...) && E2 および (E1 || ...) || E2 です (それぞれ、E1 が未展開のパックを含む場合)、または
  • E1 && (... && E2) および E1 || (... || E2) です (それぞれ、それ以外の場合)。
(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 を「包含する」とは、PQ を論理的に含意することが、P と Q のアトミック制約の同一性までで証明できる場合を指します。(型と式は同等性を分析されません: N > 0N >= 0 を包含しません)。

具体的には、まず P は選言標準形に変換され、Q は連言標準形に変換されます。PQ を包含するのは、以下の条件がすべて満たされる場合に限ります。

  • P の選言標準形の各選言句が、Q の連言標準形の各連言句を包含する。ここで、
  • 選言句が連言句を包含するのは、選言句にアトミック制約 U があり、連言句にアトミック制約 V があって、UV を包含する場合にのみ限る。
  • アトミック制約 A がアトミック制約 B を包含するのは、上記で説明したルールを用いてそれらが同一である場合にのみ限る。
  • 畳み込み展開された制約 A が別の畳み込み展開された制約 B を包含するのは、両者が同じ畳み込み演算子を持ち、A の制約 CB の制約を包含し、両方の C が同等の未展開パックを含む場合に限ります。
(C++26以降)

包含関係は制約の半順序を定義し、これは以下の決定に使用されます。

宣言 D1D2 が制約されており、D1 に関連付けられた制約が D2 に関連付けられた制約を包含する場合 (または D2 が制約されていない場合)、D1D2 と「少なくとも同程度に制約されている」と言われます。D1D2 と少なくとも同程度に制約されており、D2D1 と少なくとも同程度に制約されていない場合、D1D2 よりも「より制約されている」と言われます。

以下のすべての条件が満たされる場合、非テンプレート関数 F1 は非テンプレート関数 F2 よりも「より部分順序制約されている」と言われます。

  • 両者は同じパラメータ型リストを持ちます。ただし、明示的オブジェクトパラメータの型は除外します(C++23 以降)
  • メンバー関数である場合、両者は同じクラスの直接メンバーです。
  • 両方が非静的メンバー関数である場合、オブジェクトパラメータの型は同じです。
  • F1F2 よりも制約されています。
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) 条件付きで自明な特殊メンバー関数

[編集] キーワード

concept, requires, typename

[編集] 欠陥報告

以下の動作変更を伴う欠陥報告が、以前に公開されたC++標準に遡って適用されました。

DR 適用対象 公開された動作 正しい動作
CWG 2428 C++20 属性をコンセプトに適用できなかった 許可

[編集] 関連項目

Requires 式(C++20) 制約を記述する bool 型の prvalue 式を生成します[編集]
English 日本語 中文(简体) 中文(繁體)