约束与概念 (C++20 起)

来自cppreference.com
< cpp‎ | language


 
 
 
 

类模板函数模板(包括泛型 lambda),以及其他模板化函数(通常为类模板的成员),可以与一项约束 相关联,它指定了对模板实参的一些要求,这些要求可以被用于选择最恰当的函数重载和模板特化。

这种要求的具名集合被称为概念 。每个概念都是一个谓词,它在编译时求值,并在将之用作约束时成为模板接口的一部分:

#include <cstddef>#include <concepts>#include <functional>#include <string>   // 概念 Hashable 的声明可以被符合以下条件的任意类型 T 满足:// 对于 T 类型的值 a,表达式 std::hash<T>{}(a) 可以编译并且它的结果可以转换到 std::size_ttemplate<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());// 没有概念时典型编译器的诊断:// 二元表达式的操作数非法 ('std::_List_iterator<int>' 和 'std::_List_iterator<int>')// std::__lg(__last - __first) * 2);// ~~~~~~ ^ ~~~~~~~// …… 50 行输出……//// 有概念时典型编译器的诊断:// 错误:无法以 std::_List_iterator<int> 调用 std::sort// 注意:未满足概念 RandomAccessIterator<std::_List_iterator<int>>

概念的目的是塑造语义分类(Number、Range、RegularFunction)而非语法上的限制(HasPlus、Array)。按照 ISO C++ 核心方针 T.20 所说,“与语法限制相反,真正的概念的一个决定性的特征是有能力指定有意义的语义。”

目录

[编辑]概念

概念是要求的具名集合。概念的定义必须在命名空间作用域中出现。

概念定义拥有以下形式:

template <模板形参列表>

concept概念名 属性 (可选)=约束表达式;

属性 - 任意数量的属性的序列
// 概念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

概念在作为以下内容的一部分时也可以在类型约束中被命名:

概念在 类型约束 中接受的实参要比它的形参列表要求的要少一个,因为按语境推导出的类型会隐式地用作第一个实参:

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> 约束

[编辑]约束

约束是逻辑操作和操作数的序列,它指定了对模板实参的要求。它们可以在 requires 表达式中出现,也可以直接作为概念的主体。

(C++26 前)(C++26 起)种类型的约束:

1) 合取
2) 析取
3) 原子(不可分割)约束
4) 折叠展开约束
(C++26 起)

对包含遵循以下顺序的操作数的逻辑与表达式进行规范化,确定与一个声明关联的约束:

这个顺序决定了在检查是否满足时各个约束时的实例化顺序。

[编辑]重复声明

受约束的声明只能以相同的语法形式重声明。不要求诊断:

// 前两条 f 的声明没有问题template<Incrementable T>void f(T) requires Decrementable<T>;   template<Incrementable T>void f(T) requires Decrementable<T>;// OK:重声明   // 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>constexprbool 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>()}

[编辑]析取

两个约束的析取,是通过在约束表达式中使用 || 运算符来构成的:

如果其中一个约束得到满足,那么两个约束的析取得到满足。析取从左到右短路求值(如果满足左侧约束,那么就不会尝试对右侧约束进行模板实参替换)。

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{returntrue;}};   template<typename T> requires (S<T>{})void f(T);// #1   void f(int);// #2   void g(){ f(0);// 错误:检查 #1 时 S<int>{} 不具有 bool 类型,// 尽管 #2 能更好地匹配}

如果两个原子约束由在源码层面上相同的表达式组成,且它们的形参映射等价,那么认为它们等同

template<class T>constexprbool is_meowable =true;   template<class T>constexprbool 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);// 错误,有歧义:// BadMeowableCat 和 Meowable 中的 is_meowable<T>// 构成了有区别的原子约束且它们并不等同(因此它们不互相包含)   f2(0);// OK,调用 #4,它比 #3 更受约束// GoodMeowableCat 从 Meowable 获得其 is_meowable<T>}

折叠展开约束

折叠展开约束 由约束 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>;   // C++23 中,g() 的这两个重载具有不同的原子约束,不相同且不包含另一方:调用 g() 有歧义// C++26 中,折叠被展开,重载 #2 的约束(同时需要移动和复制)包含重载 #1 的约束(仅需要移动)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 的范式是 E1E2 范式的合取;
  • 表达式 E1 || E2 的范式是 E1E2 范式的析取;
  • 表达式 C<A1, A2, ... , AN>(其中 C 指名某个概念)的范式,是以 A1, A2, ... , ANC 的每个原子约束的形参映射中的 C 的对应模板形参进行替换之后,C 的约束表达式的范式。如果在这种形参映射中的替换产生了无效的类型或表达式,那么程序非良构,不要求诊断。
template<typename T> concept A = T::value||true;   template<typename U> concept B = A<U*>;// OK:规范化为以下各项的析取// - T::value(映射为 T -> U*)和// - true(映射为空)。// 映射中没有无效类型,尽管 T::value 对所有指针类型均非良构   template<typename V> concept C = B<V&>;// 规范化为以下的析取// - T::value(映射为 T-> V&*)和// - true(映射为空)。// 映射中构成了无效类型 V&* => 非良构,不要求诊断
  • 表达式 (E && ...)(... && E) 的范式是一条折叠展开约束,其中 CE 的范式,并且折叠运算符是 &&
  • 表达式 (E || ...)(... || E) 的范式是一条折叠展开约束,其中 CE 的范式,并且折叠运算符是 ||
  • 表达式 (E1 && ... && E2)(E1 || ... || E2) 的范式分别是:
  • (E1 && ...)&& E2 的范式和 (E1 || ...)|| E2 的范式(如果 E1 含有未展开的包)
  • E1 &&(... && E2) 的范式和 E1 ||(... || E2) 的范式(如果 E1 不含未展开的包)
(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 表达式

表达式必须具有下列形式之一:

template<class T>constexprbool is_meowable =true;   template<class T>constexprbool is_purrable(){returntrue;}   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 表达式进行替换,直到剩下原子约束的合取与析取的序列为止。

如果根据约束 P 和约束 Q 中的各原子约束的同一性可以证明 P蕴含Q,那么称 P归入Q。(并不进行类型和表达式的等价性分析:N > 0 并不归入 N >= 0)。

具体来说,首先转换 P 为析取范式并转换 Q 为合取范式。当且仅当以下情况下 P 归入 Q

  • P 的析取范式中的每个析取子句都能归入 Q 的合取范式中的每个合取子句,其中
  • 当且仅当析取子句中存在原子约束 U 而合取子句中存在原子约束 V,使得 U 归入 V 时,析取子句能归入合取子句;
  • 当且仅当使用上文所述的规则判定为等同时,称原子约束 A 能归入原子约束 B
  • 当折叠展开约束 AB 具有相同的折叠运算符,A 的约束 C 归入 B 的约束 C,并且两个 C 均包含等价的未展开包时,称折叠展开约束 A 能归入折叠展开约束 B
(C++26 起)

归入关系定义了约束的偏序,用于确定:

如果声明 D1D2 均受约束,且 D1 关联的约束能归入 D2 关联的约束,(或 D2 没有约束),那么称 D1D2 相比至少一样受约束。如果 D1 至少与 D2 一样受约束,而 D2 并非至少与 D1 一样受约束,那么 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 能归入 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_concepts201907L(C++20)约束
202002L(C++20)有条件平凡的特殊成员函数

[编辑]关键词

template, class, typename, concept, requires(C++20 起)

[编辑]缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 2428 C++20 不能将属性应用到概念 可以应用

[编辑]参阅

requires 表达式(C++20) 产生描述约束的 bool 类型表达式[编辑]
close