名稱空間
變體
操作

約束與概念

來自 cppreference.com
< cpp‎ | 實驗性


本頁描述了一個實驗性的核心語言特性。有關標準庫規範中使用的命名型別要求,請參見命名要求

類模板函式模板和非模板函式(通常是類模板的成員)可以關聯一個約束,該約束指定對模板引數的要求,可用於選擇最合適的函式過載和模板特化。

約束還可以用於將變數宣告和函式返回型別中的自動型別推導限制為僅滿足指定要求的型別。

此類要求的命名集合稱為概念。每個概念都是在編譯時評估的謂詞,併成為它用作約束的模板介面的一部分。

#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

概念的目的是建模語義類別(Number、Range、RegularFunction),而不是語法限制(HasPlus、Array)。根據 ISO C++ 核心準則 T.20,“指定有意義的語義的能力是真正概念的定義特徵,而不是語法約束。”

如果支援特性測試,此處描述的特性由宏常量 __cpp_concepts 指示,其值等於或大於 201507

目錄

[編輯] 佔位符

無約束佔位符 auto受約束佔位符,其形式為 concept-name < template-argument-list(可選)>,是待推導型別的佔位符。

佔位符可以出現在變數宣告中(在這種情況下,它們從初始化器中推導)或函式返回型別中(在這種情況下,它們從返回語句中推導)。

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 的地方使用,例如,在泛型 lambda 宣告中。

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

[編輯] 縮寫模板

如果函式引數列表中出現一個或多個佔位符,則函式宣告實際上是一個函式模板宣告,其模板引數列表包含每個唯一佔位符的一個發明引數,按出現順序排列。

// 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));

所有由等價受約束型別說明符引入的佔位符都具有相同的發明模板引數。但是,每個無約束說明符(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(可選)} ,在這種情況下不需要關鍵字 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; };
}

以下限制適用於函式概念:

  • 不允許使用 inlineconstexpr,函式自動為 inlineconstexpr
  • 不允許使用 friendvirtual
  • 不允許異常規範,函式自動為 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 種類型的約束:

1) 合取
2) 析取
3) 謂詞約束
4) 表示式約束(僅在requires-表示式中)
5) 型別約束(僅在requires-表示式中)
6) 隱式轉換約束(僅在requires-表示式中)
7) 引數推導約束(僅在requires-表示式中)
8) 異常約束(僅在requires-表示式中)
9) 引數化約束(僅在requires-表示式中)

前三種類型的約束可以直接作為概念的主體或作為臨時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

[編輯] 合取

約束 PQ 的合取指定為 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>;

兩個約束的合取僅當兩個約束都滿足時才滿足。合取從左到右評估並短路(如果左側約束不滿足,則不嘗試將模板引數替換到右側約束中:這可以防止由於即時上下文之外的替換而導致的失敗)。使用者定義的 operator&& 過載不允許在約束合取中使用。

[編輯] 析取

約束 PQ 的析取指定為 P || Q

兩個約束的析取在任一約束滿足時滿足。析取從左到右評估並短路(如果左側約束滿足,則不嘗試將模板引數推導到右側約束中)。使用者定義的 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 有兩種用法:

1) 引入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-表示式
2) 開始一個requires-表示式,它是一個 bool 型別的純右值表示式,描述對某些模板引數的約束。如果對應的概念滿足,則此類表示式為 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(可選) ) { 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(可選) trailing-return-type(可選) ;

並指定以下約束的合取:

1) expression 是一個有效表示式(表示式約束
2) 如果使用了 noexcept,表示式也必須是 noexcept(異常約束
3) 如果 trailing-return-type 命名了一個使用佔位符的型別,則該型別必須可以從表示式的型別中推匯出來(引數推導約束
4) 如果 trailing-return-type 命名了一個不使用佔位符的型別,則會新增另外兩個約束:
4a)trailing-return-type 命名的型別是有效的(型別約束
4b) 表示式的結果可以隱式轉換為該型別(隱式轉換約束
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(可以帶限定符)出現在以下情況時,執行概念解析:

1) 受約束型別說明符 void f(Concept); std::vector<Concept> x = ...;
2) 受約束引數 template<Concept T> void f();
3) 模板引入 Concept{T} struct X;
4) 約束表示式 template<typename T> void f() requires Concept<T>;
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.

為了執行概念解析,每個與名稱(以及限定符,如果有)匹配的概念的模板引數與一系列概念引數(它們是模板引數和萬用字元)進行匹配。萬用字元可以匹配任何種類(型別、非型別、模板)的模板引數。引數集根據上下文以不同方式構建:

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
2) 對於用作受約束型別說明符或引數的一部分的概念名稱,如果概念名稱與模板引數列表一起使用,則引數列表是一個單獨的萬用字元,後跟該引數列表。
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
3) 如果概念出現在模板引入中,則引數列表是與模板引入中的引數列表一樣長的佔位符序列。
template<typename... Ts>
concept bool C3 = true;
C3{T} void q2();     // OK: <T> matches <...Ts>
C3{...Ts} void q1(); // OK: <...Ts> matches <...Ts>
4) 如果概念作為模板-id的名稱出現,則概念引數列表正是該模板-id的引數序列。
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; // error: <wildcard, 0> matches 
                             // neither <typename T> nor <int T>
template<C2 T> struct S2; // both #1 and #2 match: error

[編輯] 約束的偏序

在任何進一步分析之前,約束透過替換每個命名概念和每個 requires 表示式的主體來規範化,直到剩下的是原子約束上的合取和析取序列,這些原子約束是謂詞約束、表示式約束、型別約束、隱式轉換約束、引數推導約束和異常約束。

如果可以證明 P 蘊含 Q,而無需分析型別和表示式的等價性(因此 N >= 0 不蘊含 N > 0),則概念 P 被認為包含概念 Q

具體來說,首先將 P 轉換為析取正規化,將 Q 轉換為合取正規化,然後按如下方式進行比較:

  • 每個原子約束 A 包含等價的原子約束 A
  • 每個原子約束 A 包含析取 A||B,但不包含合取 A&&B
  • 每個合取 A&&B 包含 A,但析取 A||B 不包含 A

包含關係定義了約束的偏序,用於確定:

如果宣告 D1D2 受約束,並且 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

[編輯] 關鍵字

concept, requires

[編輯] 編譯器支援

GCC >= 6.1 支援此技術規範(需要選項 -fconcepts)。