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

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

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

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

#include <string>
#include <cstddef>
#include <concepts>
using namespace std::literals;
 
// 概念 "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() {
  f("abc"s); // OK,std::string 满足 Hashable
  f(meow{}); // 错误:meow 不满足 Hashtable
}


在编译时检测制约违规,在模板实例化过程的早期进行,这导致错误信息更易理解。

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 < 模板形参列表 >

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 子句试图制约概念

不允许概念的显式实例化、显式特化或部分特化(不能更改制约的原初定义的意义)。

制约

制约是逻辑操作和操作数的序列,它指定对于模板实参的要求。它们可在 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() requires Decrementable<T>;
 
template<Decrementable T> 
void g() 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 ( 形参列表(可选) ) { 要求序列 }
形参列表 - 与函数声明中类似的形参的逗号分隔列表,但不允许默认实参且不能以(并非指定包展开的)省略号结尾。这些形参无存储期、连接或生存期,它们仅用于辅助进行各个要求的制定。这些形参在 要求序列 的闭 } 前处于作用域中。
要求序列 - 要求(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 表达式在其制约中含有无效类型或表达式,而它并非出现于模板化实体的声明之内,则程序非良构。

简单要求

简单要求是任意表达式语句。它断言该表达式合法。该表达式是不求值操作数;只检查语言正确性。

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));
};

类型要求

类型要求是关键词 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} -> std::convertible_to<typename T::inner>; // 表达式 *x 必须合法
                                                    // 并且 类型 T::inner 必须合法
                                                    // 并且 *x 的结果必须可以转换为 T::inner
    {x + 1} -> std::same_as<int>; // 表达式 x + 1 必须合法
                                  // 并且 std::Same<decltype((x + 1)), int> 必须被满足
                                  // 亦即,(x + 1) 必须为 int 类型的纯右值
    {x * 1} -> std::convertible_to<T>; // 表达式 x * 1 必须合法
                                       // 并且其结果必须可以转换为 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 无制约),则称 D1 与 D2 相比至少一样受制约。若 D1 至少与 D2 一样受制约,而 D2 并非至少与 D1 一样受制约,则 D1 比 D2 更受制约

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