約束與概念 (自 C++20 起)
類模板、函式模板(包括泛型 lambda 表示式)以及其他模板化函式(通常是類模板的成員)可能與一個約束相關聯,該約束指定了對模板引數的要求,可用於選擇最合適的函式過載和模板特化。
此類要求的命名集合稱為概念。每個概念都是一個在編譯時評估的謂詞,併成為它用作約束的模板介面的一部分。
#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
概念的目的是建模語義類別(Number、Range、RegularFunction),而不是語法限制(HasPlus、Array)。根據 ISO C++ 核心準則 T.20,“指定有意義語義的能力是真正概念的定義性特徵,而不是語法約束。”
目錄 |
[編輯] 概念
概念是一組命名的要求。概念的定義必須出現在名稱空間作用域內。
概念的定義形式如下:
template < template-parameter-list >
|
|||||||||
屬性 | - | 任意數量的屬性序列 |
// 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。
概念也可以在型別約束中命名,作為以下的一部分:
在型別約束中,概念的模板引數比其引數列表要求的少一個,因為上下文中推導的型別被隱式地用作概念的第一個引數。
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 表示式中,或直接作為概念的主體。
約束有三種(直到 C++26)四種(自 C++26 起)型別:
4) 摺疊展開的約束
|
(C++26 起) |
與宣告關聯的約束透過規範化邏輯 AND 表示式來確定,其運算元按以下順序排列:
- 為每個約束的型別模板引數或用約束佔位符型別宣告的非型別模板引數引入的約束表示式,按出現順序;
- 模板引數列表後面的requires 子句中的約束表示式;
- 在縮寫函式模板宣告中,為每個具有受限佔位符型別的引數引入的約束表示式;
- 尾隨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
[編輯] 合取
兩個約束的合取透過在約束表示式中使用 &&
運算子形成。
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>;
兩個約束的合取僅當兩個約束都滿足時才滿足。合取從左到右評估並短路(如果左約束不滿足,則不嘗試將模板引數替換到右約束中:這防止了由於即時上下文之外的替換導致的失敗)。
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 }
[編輯] 析取
兩個約束的析取透過在約束表示式中使用 ||
運算子形成。
兩個約束的析取如果其中任一約束滿足則滿足。析取從左到右評估並短路(如果左約束滿足,則不嘗試將模板引數替換到右約束中)。
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
[編輯] 原子約束
原子約束由表示式 E 以及從出現在 E 中的模板引數到涉及受約束實體模板引數的模板引數的對映(稱為引數對映)組成。
原子約束在約束規範化期間形成。E 從不是邏輯 AND 或邏輯 OR 表示式(它們分別形成合取和析取)。
原子約束的滿足性透過將引數對映和模板引數替換到表示式 E 中來檢查。如果替換導致無效型別或表示式,則約束不滿足。否則,E 在任何左值到右值轉換後,必須是型別為 bool 的純右值常量表達式,並且當且僅當其評估結果為 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 }
如果兩個原子約束在原始碼級別由相同的表示式形成,並且它們的引數對映等效,則認為它們是相同的。
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 }
摺疊展開的約束一個摺疊展開的約束由約束 令 N 為包展開引數中的元素數量。
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<A1, A2, ... , AN> 的正規化,其中
C
命名一個概念,是C
的約束表示式的正規化,替換C
的每個原子約束的引數對映中C
的相應模板引數為A1
,A2
, ...,AN
。如果這樣的替換到引數對映中導致無效型別或表示式,則程式格式錯誤,不需要診斷。
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
|
(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
,直到 P 和 Q 中原子約束的同一性,則稱約束 P
包含約束 Q
。(型別和表示式不進行等價分析:N > 0
不包含 N >= 0
)。
具體來說,首先將 P
轉換為析取正規化,將 Q
轉換為合取正規化。當且僅當滿足以下條件時,P
包含 Q
:
P
的析取正規化中的每個析取子句都包含Q
的合取正規化中的每個合取子句,其中:- 當且僅當析取子句中有一個原子約束
U
和合取子句中有一個原子約束V
,且U
包含V
時,析取子句包含合取子句; - 當且僅當原子約束
A
和原子約束B
使用上述規則是相同的,原子約束A
包含原子約束B
。
|
(C++26 起) |
包含關係定義了約束的偏序,用於確定:
本節不完整 原因:從上述內容到此處的反向連結。 |
如果宣告 D1
和 D2
受約束,並且 D1
相關的約束包含 D2
相關的約束(或 D2
未受約束),則稱 D1
至少與 D2
一樣受約束。如果 D1
至少與 D2
一樣受約束,而 D2
不至少與 D1
一樣受約束,則 D1
比 D2
更受約束。
如果滿足所有以下條件,則非模板函式 F1
比非模板函式 F2
更受偏序約束:
- 它們具有相同的引數型別列表,省略顯式物件引數的型別(自 C++23 起)。
- 如果它們是成員函式,則兩者都是同一類的直接成員。
- 如果兩者都是非靜態成員函式,則它們的物件引數型別相同。
-
F1
比F2
更受約束。
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) | 條件性平凡的特殊成員函式 |
[編輯] 關鍵詞
[編輯] 缺陷報告
下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。
缺陷報告 | 應用於 | 釋出時的行為 | 正確的行為 |
---|---|---|---|
CWG 2428 | C++20 | 無法將屬性應用於概念 | 允許 |
[編輯] 另見
Requires 表示式(C++20) | 生成型別為 bool 的純右值表示式,描述約束 |