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

来自cppreference.com
< cpp‎ | language
本页面描述了 C++20 接纳的核心语言特性。对于标准库中使用的具名类型要求,见具名要求。有关这个功能特性的概念 TS 版本,见此处

类模板函数模板,以及非模板函数(通常是类模板的成员),可以关联到约束(constraint),它指定了对模板实参的一些要求,这些要求可以被用于选择最恰当的函数重载和模板特化。

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

#include <string>
#include <cstddef>
#include <concepts>
 
// 概念 "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 {};
 
template<Hashable T>
void f(T); // 受约束的 C++20 函数模板
 
// 应用相同约束的另一种方式:
// template<typename T>
//     requires Hashable<T>
// void f(T); 
// 
// template<typename T>
// void f(T) requires Hashable<T>; 
 
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 表达式(见下文)中出现,也可以直接作为概念的主体。

有三种类型的约束:

1) 合取(conjunction)
2) 析取(disjunction)
3) 不可分割约束(atomic constraint)

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

  • 每个声明中受约束的类型模板形参或带占位类型声明的非类型模板形参所引入的约束表达式,按出现顺序;
  • 模板形参列表之后的 requires 子句中的约束表达式;
  • 简写函数模板声明中每个拥有受约束占位类型的形参所引入的约束表达式;
  • 尾部的 requires 子句中的约束表达式。

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

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

template<Incrementable T>
void f(T) requires Decrementable<T>;
 
template<Incrementable T>
void f(T) requires Decrementable<T>; // OK:重声明
 
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'); // 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 始终不会是逻辑与(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); // 错误:检查 #1 时 S<int>{} 不具有 bool 类型,
          // 尽管 #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); // 错误,有歧义:
           // BadMeowableCat 和 Meowable 中的 is_meowable<T>
           // 构成了有区别的不可分割约束且它们并不等同(因此它们不互相包含)
 
    f2(0); // OK,调用 #4,它比 #3 更受约束
           // GoodMeowableCat 从 Meowable 获得其 is_meowable<T>
}

约束规范化

约束规范化是将一个约束表达式变换为一个不可分割约束的合取与析取的序列的过程。表达式的范式定义如下:

  • 表达式 (E) 的范式就是 E 的范式;
  • 表达式 E1 && E2 的范式是 E1E2 范式的合取;
  • 表达式 E1 || E2 的范式是 E1E2 范式的析取;
  • 表达式 C<A1, A2, ... , AN>(其中 C 指名某个概念)的范式,是以 A1, A2, ... , AN 对 C 的每个不可分割约束的形参映射中的 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 而它的形参映射是恒等映射。这包括所有折叠表达式,甚至包括以 &&|| 运算符进行的折叠。

用户定义的 &&|| 重载在约束规范化上无效。

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>
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>(); // 错误:is_purrable<T>() 不是初等表达式
 
template<class T>
void h(T) requires (is_purrable<T>()); // OK

约束的偏序

在任何进一步的分析之前都会对各个约束进行规范化,对每个具名概念的主体和每个 requires 表达式进行替换,直到剩下不可分割约束的合取与析取的序列为止。

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

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

  • P 的析取范式中的每个析取子句都能归入 Q 的合取范式中的每个合取子句,其中
  • 当且仅当析取子句中存在不可分割约束 U 而合取子句中存在不可分割约束 V,使得 U 归入 V 时,析取子句能归入合取子句;
  • 当且仅当使用上文所述的规则判定为等同时,称不可分割约束 A 能归入不可分割约束 B

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

如果声明 D1D2 均受约束,且 D1 关联的约束能归入 D2 关联的约束,(或 D2 没有约束),那么称 D1D2 相比至少一样受约束。如果 D1 至少与 D2 一样受约束,而 D2 并非至少与 D1 一样受约束,那么 D1D2 更受约束

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) 有条件平凡的特殊成员函数

关键词

concept, requires

缺陷报告

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

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

参见

requires 表达式(C++20) 产生描述约束的 bool 类型表达式