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

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


类模板函数模板及非模板函数(常为类模板成员)可以与制约关联,制约指定模板实参上的要求,这能用于选择最准确的函数重载和模板特化。

制约亦可用于限制变量声明和函数返回类型中的自动类型推导,为只有满足指定要求的类型。

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

#include <string>
#include <cstddef>
using namespace std::literals;
 
// 概念 "Hashable" 的声明,
// 任何对于 T 类型值,表达式 std::hash<T>{}(a) 可编译,
// 而其结果可转换为 std::size_t 的类型 T 满足它
template<typename T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> 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 不满足
}


在编译时检测制约违规,早于模板实例化处理,这导致错误信息更易理解。

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 < template-parameter-list >

concept concept-name = constraint-expression;

// 概念
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) 合取
2) 析取
3) 原子制约

通过规范化运算数遵循下列顺序的逻辑与表达式,确定与声明关联的制约:

  • 按出现顺序,对每个有制约模板形参引入的制约表达式;
  • 模板形参列表后的 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 决不是逻辑与或者逻辑或表达式(这些形式递归地构成析取和合取)。

通过替换形参映射和模板实参到表达式 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 指名概念)的范式,是对 C 的每个原子制约的形参映射中 C 的对应模板形参,替换 A1, A2, ... , AN 后, 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 ( parameter-list(可选) ) { requirement-seq }
parameter-list - 逗号分隔列表,如在函数声明中,除了不允许默认参数,且最后的参数不能是省略号。这些参数无存储期、链接或生存期。这些参数在 requirement-seq 的闭 } 前处于作用域中。若不使用参数,则环绕的括号亦可省略
requirement-seq - 要求的空白符分隔序列,描述于下(每个要求以分号结尾)。每个要求添加另一制约到此 requires 表达式所定义的制约合取

requirements-seq 中的每个要求是下列之一:

  • 简单要求
  • 类型要求
  • 复合要求
  • 嵌套要求

要求可以提及在作用域中的模板形参、于 parameter-list 引入的局部参数和任何其他从外围语境可见的声明。

替换模板实参到用于模板化实体的 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)} }; 
};

[编辑] 复合要求

复合要求拥有形式

{ expression } noexcept(可选) return-type-requirement(可选) ;
return-type-requirement - trailing-return-type 或形式 -> cv1(可选) qualified-concept-name cv2(可选) abstract-declarator(可选) 之一

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

1) 替换模板实参(若存在)到 expression 中;
2) 若不用 noexcept ,则 expression 必须非潜在抛出
3)return-type-requirement 存在,则:
a) 替换模板实参到 return-type-requirement 中;
b) 若它是 trailing-return-type ,则 expression 的结果必须可隐式转换为指名的类型。若转换失败,则外围 requires 表达式为 false
c) 若它含有 qualified-concept-name ,则给定虚构函数模板 template< qualified-concept-name T > void f(cv T abstract-declarator); ,其中 cvcv1cv2 的联合,则调用 f(expression) 的模板实参推导必须成功。若推导失败,则外围 requires 表达式求值为 false
template<typename T> concept C2 =
requires(T x) {
    {*x} -> typename T::inner; // 表达式 *x 必须合法
                               // AND 类型 T::inner 必须合法
                               // AND  *x 的结果必须可转换为 T::inner
};

[编辑] 嵌套要求

嵌套要求拥有形式

requires constraint-expression ;

它能用于指定以局部参数表示的额外制约。若替换的模板实参存在,则它们必须满足 constraint-expression 。替换模板实参到嵌套要求中,导致到 constraint-expression 中的替换仅为确定 constraint-expression 是否得到满足所需的外延。

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 (故 N >= 0 不包含 N > 0

具体来说,首先转换 P 为析取范式并转换 Q 为合取范式。 P 包含 Q 若且唯若:

  • P 的析取范式中的每个析取子句都包含 Q 的合取范式中的每个合取子句,其中
  • 析取子句包含合取子句,若且唯若析取子句中有原子制约 U 而合取子句中有原子制约 V ,使得 U 包含 V
  • 原子制约 A 包含原子制约 B ,若且唯若它们用上述规则等同。

包含关系定义制约的偏序,用于确定:

若声明 D1D2 有制约且 D1 的正规化制约包含 D2 的正规化制约(或若 D1 有制约而 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