约束与概念 (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 表达式

关键词 requires 也用来开始一个 requires 表达式,它是 bool 类型的纯右值表达式,描述对一些模板实参的约束。这种表达式在约束得到满足时是 true,否则是 false

template<typename T>
concept Addable = requires (T x) { x + x; }; // requires 表达式
 
template<typename T> requires Addable<T> // requires 子句,不是 requires 表达式
T add(T a, T b) { return a + b; }
 
template<typename T>
    requires requires (T x) { x + x; } // 随即的约束,注意关键字被使用两次
T add(T a, T b) { return a + b; }

requires 表达式的语法如下:

requires { 要求序列 }
requires ( 形参列表(可选) ) { 要求序列 }
形参列表 - 与函数声明中类似的以逗号分隔的形参列表,但不能有默认实参且不能以(并非指定包展开的)省略号结尾。这些形参没有存储期、连接或生存期,它们只会用来辅助进行各个要求的制定。这些形参在 要求序列 的闭 } 前处于作用域中。
要求序列 - 要求(requirement)的序列,描述于下(每个要求以分号结尾)。

要求序列 中的每个要求是下列之一:

  • 简单要求(simple requirement)
  • 类型要求(type requirement)
  • 复合要求(compound requirement)
  • 嵌套要求(nested requirement)

要求可以提及处于作用域中的模板形参,由 形参列表 引入的局部形参,以及从它的外围语境中可见的任何其他声明。

模板化实体的声明中所使用的 requires 表达式进行模板实参替换,可能导致在其要求中形成无效的类型或表达式,或违反这些要求的语义约束。这些情况下,该 requires 表达式求值为 false 而不会导致程序非良构。替换和语义约束检查按词法顺序执行,并在遇到确定 requires 表达式结果的条件时停止。如果替换(如果存在)和语义约束检查成功,那么 requires 表达式求值为 true

如果对于每一种可能的模板实参 requires 表达式中都会出现替换失败,那么程序非良构,不要求诊断:

template<class T>
concept C = requires
{
    new int[-(int)sizeof(T)]; // 对每个 T 均为无效:非良构,不要求诊断
};

如果 requires 表达式在它的约束中含有无效的类型或表达式,而它并不在模板化实体的声明内出现,那么程序非良构。

简单要求

简单要求是不以 requires 关键词起始的任意表达式语句。它断言该表达式合法。该表达式是不求值操作数;只检查语言正确性。

template<typename T>
concept Addable = requires (T a, T b)
{
    a + b; // “表达式 a + b 是可编译的合法表达式”
};
 
template <class T, class U = T>
concept Swappable = requires(T&& t, U&& u)
{
    swap(std::forward<T>(t), std::forward<U>(u));
    swap(std::forward<U>(u), std::forward<T>(t));
};

requires 关键词起始的要求始终会被解读成嵌套要求。因此简单要求不能以无括号的 requires 表达式起始。

类型要求

类型要求是关键词 typename 后随一个可以有限定的类型名。它的要求是该类型名合法:这可以用来校验某个具名嵌套类型存在,或某个类模板特化指名一个类型,或某个别名模板特化指名一个类型。指名类模板特化的类型要求不要求该类型完整。

template<typename T>
using Ref = T&;
 
template<typename T>
concept C = requires
{
    typename T::inner; // 要求的嵌套成员名
    typename S<T>;     // 要求的类模板特化
    typename Ref<T>;   // 要求的别名模板替换
};
 
template <class T, class U>
using CommonType = std::common_type_t<T, U>;
 
template <class T, class U>
concept Common = requires (T t, U u)
{
    typename CommonType<T, U>; // CommonType<T, U> 合法并指名一个类型
    { CommonType<T, U>{std::forward<T>(t)} }; 
    { CommonType<T, U>{std::forward<U>(u)} }; 
};

复合要求

复合要求的形式为

{ 表达式 } noexcept(可选) 返回类型要求(可选) ;
返回类型要求 - -> 类型约束

并断言该具名表达式的各项性质。以下列顺序进行替换和语义约束检查:

1) 替换模板实参(若存在)到 表达式 中;
2) 如果使用了 noexcept,那么 表达式 必须不会抛出
3) 如果出现了 返回类型规定,那么:
a) 替换模板实参到 返回类型规定 中;
b) decltype((表达式)) 必须满足有该 类型约束 所蕴含的约束。否则,外围 requires 表达式为 false
template<typename T>
concept C2 = requires(T x)
{
    // 表达式 *x 必须合法
    // 并且类型 T::inner 必须合法
    // 并且 *x 的结果必须可以转换为 T::inner
    {*x} -> std::convertible_to<typename T::inner>;
 
    // 表达式 x + 1 必须合法
    // 并且 std::Same<decltype((x + 1)), int> 必须被满足
    // 也就是说,(x + 1) 必须是 int 类型的纯右值
    {x + 1} -> std::same_as<int>;
 
    // 表达式 x * 1 必须合法
    // 并且它的结果必须可以转换到 T
    {x * 1} -> std::convertible_to<T>;
};

嵌套要求

嵌套要求的形式为

requires 约束表达式 ;

它可以用来以局部形参来指定额外的约束。约束表达式 必须被所替换的模板实参(如果存在)所满足。在嵌套要求中进行模板实参的替换所导致的在 约束表达式 中的替换只会进行到足以确定 约束表达式 是否得到满足所需的程度。

template <class T>
concept Semiregular = DefaultConstructible<T> &&
    CopyConstructible<T> && Destructible<T> && CopyAssignable<T> &&
requires(T a, size_t n)
{  
    requires Same<T*, decltype(&a)>; // 嵌套:“Same<...> 求值为 true”
    { a.~T() } noexcept; // 复合:"a.~T()" 是不抛出的合法表达式
    requires Same<T*, decltype(new T)>; // 嵌套:“Same<...> 求值为 true”
    requires Same<T*, decltype(new T[n])>; // 嵌套
    { delete new T }; // 复合
    { delete new T[n] }; // 复合
};

约束的偏序

在任何进一步的分析之前都会对各个约束进行规范化,对每个具名概念的主体和每个 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); // 有歧义

关键词

concept, requires