Constraints and concepts (since C++20)
类模板 、 函数模板 (包括 泛型lambda )以及其他 模板化函数 (通常是类模板的成员)可能会关联一个 约束 ,该约束规定了模板参数必须满足的要求,可用于选择最合适的函数重载和模板特化。
这类 要求 的命名集合被称为 concept 。每个concept都是一个在编译时求值的谓词,当它被用作约束时,就会成为模板接口的组成部分:
#include <cstddef> #include <concepts> #include <functional> #include <string> // 概念“Hashable”的声明,满足任意类型“T” // 使得对于类型“T”的值“a”,表达式 std::hash<T>{}(a) // 能够编译且其结果可转换为 std::size_t template<typename T> concept Hashable = requires(T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>; }; struct meow {}; // 受约束的 C++20 函数模板: template<Hashable T> void f(T) {} // // 应用相同约束的替代方法: // template<typename T> // requires Hashable<T> // void f(T) {} // // template<typename T> // void f(T) requires Hashable<T> {} // // void f(Hashable auto /* 参数名 */) {} int main() { using std::operator""s; 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
概念的设计意图是建模语义范畴(Number、Range、RegularFunction),而非语法限制(HasPlus、Array)。根据 ISO C++核心准则 T.20 ,“能够定义具有实际意义的语义,是真正概念与语法约束的本质区别。”
目录 |
概念
概念是命名的一组 要求 集合。概念的定义必须出现在命名空间作用域。
概念的定义具有以下形式
template <
模板形参列表
>
|
|||||||||
| attr | - | 任意数量的 属性 序列 |
// 概念 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 子句试图约束概念
不允许对概念进行显式实例化、显式特化或部分特化(不能更改约束原始定义的含义)。
概念可以在id表达式中命名。该id表达式的值为 true (若约束表达式得到满足),否则为 false 。
概念也可以在类型约束中命名,作为
在 类型约束 中,概念所需的模板参数比其参数列表要求的少一个,因为上下文推导的类型被隐式用作概念的第一个参数。
template<class T, class U> concept Derived = std::is_base_of<U, T>::value; template<Derived<Base> T> void f(T); // T 受 Derived<T, Base> 约束
约束条件
约束(constraint)是由逻辑运算符和操作数组成的序列,用于规定模板参数必须满足的要求。它们可以出现在 requires 表达式 中,或直接作为概念(concept)的定义体。
存在 三种 (until C++26) 四种 (since C++26) 约束类型:
|
4)
折叠展开的约束
|
(since C++26) |
与声明关联的约束是通过 规范化 一个逻辑与表达式来确定的,该表达式的操作数按以下顺序排列:
- 为每个带有约束的 类型模板参数 或使用约束的 占位符类型 声明的常量模板参数引入的约束表达式,按出现顺序排列;
- 模板参数列表后的 requires 子句 中的约束表达式;
- 在 简写函数模板 声明中,为每个带有约束的 占位符类型 参数引入的约束表达式;
- 尾随的 requires 子句 中的约束表达式。
此顺序决定了检查约束满足性时实例化约束的先后次序。
重声明
受约束的声明只能使用相同的语法形式进行重声明。不要求诊断:
// 前两个 f 的声明是正确的 template<Incrementable T> void f(T) requires Decrementable<T>; template<Incrementable T> void f(T) requires Decrementable<T>; // 正确,属于重声明 // 包含第三个逻辑等价但语法不同的 f 声明是病式的,不要求诊断 template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // 以下两个声明具有不同的约束: // 第一个声明包含 Incrementable<T> && Decrementable<T> // 第二个声明包含 Decrementable<T> && Incrementable<T> // 尽管它们在逻辑上是等价的 template<Incrementable T> void g(T) requires Decrementable<T>; template<Decrementable T> void g(T) requires Incrementable<T>; // 病式,不要求诊断
合取约束
两个约束的合取是通过在约束表达式中使用
&&
运算符构成的:
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'); // 正确:调用 #2。在检查 #1 的约束时, // 'sizeof(char) > 1' 条件不满足,因此不会检查 get_value<T>() }
析取约束
两个约束条件的析取通过使用约束表达式中的
||
运算符构成。
两个约束的析取关系在任一约束满足时即成立。析取运算从左向右求值且采用短路求值(若左侧约束已满足,则不会尝试对右侧约束进行模板参数替换)。
template<class T = void> requires EqualityComparable<T> || Same<T, void> struct equal_to;
原子约束
原子约束由一个表达式 E 和从出现在 E 内的模板参数到受约束实体的模板参数所涉及的模板实参的映射组成,称为其 参数映射 。
原子约束在 约束规范化 过程中形成。 E 永远不会是逻辑与或逻辑或表达式(这些分别形成合取式和析取式)。
原子约束的满足性检查通过将参数映射和模板实参代入表达式 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); // 错误:检查 #1 时 S<int>{} 不具有 bool 类型, // 即使 #2 是更优的匹配 }
两个原子约束如果是在源代码层面由相同表达式构成且其参数映射等价,则被视为 identical (等同)。
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); // 正确:调用 #4,比 #3 更具约束性 // GoodMeowableCat 从 Meowable 获取了其 is_meowable<T> }
折叠展开约束
折叠展开约束由约束
设 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
|
(since C++26) |
约束规范化
约束归一化 是将约束表达式转换为原子约束的合取与析取序列的过程。表达式的 规范形式 定义如下:
- 表达式 ( E ) 的规范形式是 E 的规范形式。
- 表达式 E1 && E2 的规范形式是 E1 和 E2 规范形式的合取。
- 表达式 E1 || E2 的规范形式是 E1 和 E2 规范形式的析取。
-
表达式
C
<
A1, A2, ... , AN
>
(其中
C命名一个概念)的规范形式是:将A1、A2、...、AN代入C的各个原子约束的参数映射中对应模板参数后,所得C的约束表达式的规范形式。若任何此类代入操作导致参数映射中出现无效类型或表达式,则程序非良构,不要求诊断。
template<typename T> concept A = T::value || true; template<typename U> concept B = A<U*>; // 正常:规范化为以下析取的组合 // - T::value(映射关系 T -> U*)与 // - true(使用空映射) // 即使 T::value 对所有指针类型均不合法,映射中仍不会出现无效类型 template<typename V> concept C = B<V&>; // 规范化为以下析取的组合 // - T::value(映射关系 T-> V&*)与 // - true(使用空映射) // 映射中形成无效类型 V&* => 病式构造(NDR)
|
(since C++26) |
-
任何其他表达式
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>; // 正确 template<class T> void g(T) requires is_purrable<T>(); // 错误:is_purrable<T>() 不是主表达式 template<class T> void h(T) requires (is_purrable<T>()); // 正确
约束的部分排序
在进行任何进一步分析之前,约束会通过替换每个命名概念的主体和每个 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。
|
(since 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 包含 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); // 存在歧义
注释
| 功能测试宏 | 值 | 标准 | 功能特性 |
|---|---|---|---|
__cpp_concepts
|
201907L
|
(C++20) | 约束 |
202002L
|
(C++20) | 条件性平凡的 特殊成员函数 |
关键词
缺陷报告
下列行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 适用范围 | 发布时行为 | 正确行为 |
|---|---|---|---|
| CWG 2428 | C++20 | 无法对概念应用属性 | 允许 |
参见
| Requires表达式 (C++20) | 生成一个类型为 bool 的纯右值表达式,用于描述约束条件 |