制约与概念

来自cppreference.com
< cpp‎ | language

此页面描述实验性核心语言特性。对于用于标准库规范中的具名类型要求,见库概念

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

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

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

#include <string>
#include <locale>
using namespace std::literals;
 
// 概念 "EqualityComparable" 的声明,这为任何对于二个类型 T 的值 a 与 b ,
// 表达式 a==b 通过编译且结果可转换为 bool 的类型 T 所满足
template<typename T>
concept EqualityComparable = requires(T a, T b) {
    { a == b } -> bool;
};
 
template<EqualityComparable T>
void f(T&&); // 受制约的 C++20 函数模板
 
void f(EqualityComparable&&); // 有制约函数模板的声明
// template<typename T>
// void f(T&&) requires EqualityComparable<T>; // 相同的长形式
 
// void f(EqualityComparable&&); // 相同的短形式(仅概念 TS ,非 C++ 20 )
 
int main() {
  f("abc"s); // OK : std::string 为 EqualityComparable
  f(std::use_facet<std::ctype<char>>(std::locale{})); // 错误:非 EqualityComparable 
}


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

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 ,“与语法制约相反,指定有意义语义的能力是真概念的定义性特征。”

若支持特性测试,则以值大于或等于 201507 的宏常量 __cpp_concepts 描述于此节的特性(发布于 2015 年的概念 TS )。

目录

占位符

无制约占位符 auto 与拥有形式 concept-name < template-argument-list(optional)>有制约占位符,是要被推导的类型的占位符。

占位符可出现于变量声明(该情况下它们从初始化器推导)或于函数返回类型(该情况下它们从 return 语句推导)

std::pair<auto, auto> p2 = std::make_pair(0, 'a'); // 第一个 auto 是 int,
                                                   // 第二个 auto 是 char
 
Sortable x = f(y); // x 的类型从 f 的返回类型推导,仅若类型满足制约 Sortable 才能编译
 
auto f(Container) -> Sortable; // 返回类型从 return 语句推导,仅若类型满足 Sortable 才能编译

占位符亦可出现于形参中,该情况下它们将函数声明转化为模板声明(若占位符有制约,则模板声明有制约)

template<size_t N> concept bool Even = (N%2 == 0);
void f(std::array<auto, Even>); // 这是有二个形参的模板:
       // 无制约类型形参和有制约非类型形参

有制约占位符可用于 auto 能用的任何位置,例如在泛型 lambda 声明中

auto gl = [](Assignable& a, auto* b) { a = *b; };

缩写的模板

若函数参数列表中出现一或多个占位符,则函数声明实际上是函数模板声明,其模板形参列表以出现顺序,为每个单独的占位符包含一个虚设的形参

// 短形式
void g1(const EqualityComparable*, Incrementable&);
// 长形式:
// template<EqualityComparable T, Incrementable U> void g1(const T*, U&);
// 更长形式:
// template<typename T, typename U>
// void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>;
 
void f2(std::vector<auto*>...);
// 长形式: template<typename... T> void f2(std::vector<T*>...);
 
void f4(auto (auto::*)(auto));
// 长形式: template<typename T, typename U, typename V> void f4(T (U::*)(V));

等价的有制约类型指定符所引入的所有占位符拥有同一虚设模板形参。然而,每个无制约指定符( auto )始终引入已被不同的模板形参

void f0(Comparable a, Comparable* b);
// 长形式: template<Comparable T> void f0(T a, T* b);
 
void f1(auto a, auto* b);
// 长形式: template<typename T, typename U> f1(T a, U* b);

函数模板与类模板都能用模板引入声明,它有语法 concept-name { parameter-list(可选)} ,此情况中不需要关键词 template :来自模板引入的 parameter-list 的每个形参都成为模板形参,其种类(类型、非类型、模板)以具名概念中对应形参的种类确定。

除了声明模板,模板引入关联一个谓词制约(见后述),它指名(对于变量概念)或调用(对于函数概念)该引入所指名的概念。

EqualityComparable{T} class Foo;
// 长形式: template<EqualityComparable T> class Foo;
// 更长形式: template<typename T> requires EqualityComparable<T> class Foo;
 
template<typename T, int N, typename... Xs> concept bool Example = ...;
Example{A, B, ...C} struct S1;
// 长形式: template<class A, int B, class... C> requires Example<A,B,C...> struct S1;

对于函数模板,模板引入能与占位符组合:

Sortable{T} void f(T, auto);
// 长形式: template<Sortable T, typename U> void f(T, U);
// 另一种只用占位符的形式: void f(Sortable, auto);
(概念 TS)

[编辑] 概念

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

概念的定义拥有 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 试图制约概念定义
(C++20 起)

概念的定义拥有函数模板定义(该情况下称为函数概念)或变量模板定义(该情况下称为变量概念)的形式。仅有的区别是关键词 concept 出现于 decl-specifier-seq 中:

// 变量概念
template <class T, class U>
concept bool Derived = std::is_base_of<U, T>::value;
 
// 函数概念
template <class T>
concept bool EqualityComparable() { 
    return requires(T a, T b) { {a == b} -> Boolean; {a != b} -> Boolean; };
}

下列制约应用于函数概念:

  • 不允许 inlineconstexpr ,函数自动为 inlineconstexpr
  • 不允许 friendvirtual
  • 不允许异常规定,函数自动为 noexcept(true)
  • 不能声明并延迟定义,不能重声明
  • 返回类型必须是 bool
  • 不允许返回类型推导
  • 参数列表必须为空
  • 函数体必须仅由一条 return 语句组成,其参数必须是一条制约表达式(谓词制约、其他制约的合取/析取或 requires 表达式,见后述)

下列制约应用于变量概念:

  • 必须有类型 bool
  • 不能声明为无初始化器
  • 不允许 constexpr ,变量自动为 constexpr
  • 初始化器必须是制约表达式(谓词制约、其他制约的合取/析取或 requires 表达式,见后述)

概念不能在函数体内或变量初始化器内递归地指涉自身:

template<typename T>
concept bool F() { return F<typename T::type>(); } // 错误
template<typename T>
concept bool V = V<T*>; // 错误

概念定义不能有关联的制约。

template<class T> concept bool C1 = true;
template<C1 T>
concept bool Error1 = true; // 错误:C1 T 声明了一个关联的制约
template<class T> requires C1<T>
concept bool Error2 = true; // 错误:requires 子句声明了一个关联的制约
(概念 TS)

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

[编辑] 制约

制约是一系列逻辑运算,它指定模板实参上的要求。它们可出现于 requires 表达式(见后述)中,及直接作为概念的体。

制约有 9 种类型:

1) 合取
2) 析取
3) 谓词制约
4) 表达式制约(仅在 requires 表达式中)
5) 类型制约(仅在 requires 表达式中)
6) 隐式转换制约(仅在 requires 表达式中)
7) 实参推导制约(仅在 requires 表达式中)
8) 异常制约(仅在 requires 表达式中)
9) 参数化制约(仅在 requires 表达式中)

前三个类型的制约可以直接作为概念的体,或作为专门的 requires 子句出现:

template<typename T>
requires // requires 子句(额外制约)
sizeof(T) > 1 && get_value<T>() // 二个谓词制约的合取
void f(T);

附着多个制约到同一声明时,总制约是以下列顺序的合取:模板引入所引入的制约、按出现顺序的每个模板形参的制约、模板形参列表后的 requires 子句、按出现顺序的每个函数参数的制约、尾随 requires 子句:

// 声明用制约 Incrementable<T> && Decrementable<T> 声明同一有制约函数模板 
template<Incrementable T>
void f(T) requires Decrementable<T>;
template<typename T>
requires Incrementable<T> && Decrementable<T>
void f(T); // 概念 TS 中 OK , C++20 中病式而不要求诊断
 
// 下列二个声明拥有不同制约:
// 第一个声明有 Incrementable<T> && Decrementable<T>
// 第二个声明有 Decrementable<T> && Incrementable<T>
// 尽管它们逻辑等价。第二个声明为病式,不要求诊断。
 
template<Incrementable T> requires Decrementable<T> void g();
template<Decrementable T> requires Incrementable<T> void g(); // 错误

[编辑] 合取

制约 PQ 的合取指定为 P && Q

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

仅若二个制约均得到满足,二个制约的合取才得到满足。合取从左到右并短路求值(若不满足左制约,则不尝试到右制约的模板实参替换:这阻止立即语境之外替换导致的失败)。制约合取中不允许用户定义的 operator&& 重载。

[编辑] 析取

制约 PQ 的析取指定为 P || Q

若任一制约得到满足,则二个制约的析取的到满足。析取从左到右并短路求值(若满足左制约,则不尝试到右制约的模板实参替换)。制约析取中不允许用户定义的 operator|| 重载。

template <class T = void>
requires EqualityComparable<T>() || Same<T, void>
struct equal_to;

[编辑] 谓词制约

谓词制约是 bool 类型的常量表达式。它仅若求值为 true 才得到满足。

template<typename T> concept Size32 = sizeof(T) == 4;

谓词制约能指定非类型模板形参和模板模板实参上的制约。

谓词制约必须直接求值为 bool ,不允许转换:

template<typename T> struct S {
    constexpr explicit operator bool() const { return true; }
};
template<typename T>
requires S<T>{} // 错误的谓词制约: S<T>{} 不是 bool
void f(T);
f(0); // 错误:制约决不被满足

[编辑] 要求

关键词 requires 以二种方式使用:

1) 引入 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 表达式

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

  • 初等表达式,例如 Swappable<T>std::is_integral<T>::value(std::is_object_v<Args> && ...)
  • 以运算符 && 连接的初等表达式序列
  • 以运算符 || 连接的前述表达式序列
(C++20 起)
2) 开始 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 表达式引入一个参数化制约

替换模板实参到至 reqiures 表达式可能导致于其要求中形成非法类型或表达式。这些情况下,

  • 若替换失败发生于用于模板化实体声明之外的 requires 表达式,则程序为病式。
  • 若 requires 表达式用于模板化实体的声明中,则对应的制约被当做“不满足”且替换失败不是错误,然而
  • 若替换失败会对每个可能的模板实参都发生,则程序为病式,不要求诊断:
template<class T> concept C = requires {
    new int[-(int)sizeof(T)]; // 对每个 T 非法:病式,不要求诊断
};

[编辑] 简单要求

简单要求是任意表达式语句。要求是表达式为合法(此为表达式制约)。不同于谓词制约,不发生求值,只检查语言正确性。

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(可选) trailing-return-type(可选) ;

并指定下列制约的合取:

1) expression 是合法表达式(表达式制约
2) 若使用 noexcept ,则表达式必须亦为 noexcept (异常制约
3)trailing-return-type 指名用占位符的类型,则类型必须可从表达式的类型推导(实参推导制约
4)trailing-return-type 指名不用占位符的类型,则再添加二个制约:
4a) trailing-return-type 所指名的类型合法(类型制约
4b) 表达式结果可隐式转换到该类型(隐式转换制约
template<typename T> concept C2 =
requires(T x) {
    {*x} -> typename T::inner; // 表达式 *x 必须合法
                               // 与 类型 T::inner 必须合法
                               // 与  *x 的结果必须可转换为 T::inner
};
 
template <class T, class U> concept Same = std::is_same<T,U>::value;
template <class B> concept Boolean =
requires(B b1, B b2) {
    { bool(b1) }; // 直接初始化制约必须使用表达式
    { !b1 } -> bool; // 复合制约
    requires Same<decltype(b1 && b2), bool>; // 嵌套制约,见后述
    requires Same<decltype(b1 || b2), bool>;
};

[编辑] 嵌套制约

嵌套制约是以分号结尾的另一 requires 子句。这用于引入以应用于局部参数的其他具名概念表达的谓词制约(见上述)(在 requires 子句外,谓词制约不能用参数,而直接在 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] }; // 复合
};

概念决议

类似任何其他函数模板,函数概念(但不是变量概念)能重载:可以提供全部使用同一 concept-name 的多个概念定义。

概念决议在 concept-name (可以有限定)出现于下列语境时进行

1) 有制约类型指定符 void f(Concept); std::vector<Concept> x = ...;
2) 有制约形参 template<Concept T> void f();
3) 模板引入 Concept{T} struct X;
4) 制约表达式 template<typename T> void f() requires Concept<T>;
template<typename T> concept bool C() { return true; } // #1
template<typename T, typename U> concept bool C() { return true; } // #2
void f(C); // C 所指代的制约集包含 #1 和 #2 ;
           // 概念决议(见后述)选择 #1 。

为进行概念决议,每个匹配名称(与限定,若存在)的模板形参都与模板实参与通配符这些概念实参的序列配对。通配符可匹配任何种类(类型、非类型、模板)的模板实参。形参集构造方式各异,依赖于语境

1) 对于用作有制约类型指定符或形参一部分的概念名,若概念名以无形参列表使用,则实参列表是单个通配符。
template<typename T> concept bool C1() { return true; } // #1
template<typename T, typename U> concept bool C1() { return true; } // #2
void f1(const C1*); // <wildcard> 匹配 <T> ,选择 #1
2) 对于用作有制约类型指定符或形参一部分的概念名,若概念名以模板实参列表使用,则实参列表是一个通配符后随实参列表。
template<typename T> concept bool C1() { return true; } // #1
template<typename T, typename U> concept bool C1() { return true; } // #2
void f2(C1<char>); // <wildcard, char> 匹配 <T, U> ,选择 #2
3) 若概念出现于模板引入中,则实参列表是与模板引入中形参列表等长的占位符序列
template<typename... Ts>
concept bool C3 = true;
C3{T} void q2();     // OK: <T> 匹配 <...Ts>
C3{...Ts} void q1(); // OK: <...Ts> 匹配 <...Ts>
4) 若概念作为模板 id 出现,则概念实参列表准确地是该模板 id 的实参序列
template<typename T> concept bool C() { return true; } // #1
template<typename T, typename U> concept bool C() { return true; } // #2
 
template <typename T>
void f(T) requires C<T>(); // 匹配 #1

概念决议通过配对每个实参和对应每个可见概念的对应形参进行。默认模板实参(若使用)为每个不对应实参的形参实例化,然后后附到实参列表。模板实参匹配形参,仅若它拥有相同种类(类型、非类型、模板),除非实参是通配符。形参包可匹配零或更多实参,只要所有实参都匹配种类中的模式(除非它们是通配符)。

若任何实参不匹配其对应的形参,或若有多于形参的实参,且最后的形参不是包,则该概念不可生成。若有零或多于一个可生成概念,则程序为病式。

template<typename T> concept bool C2() { return true; }
template<int T> concept bool C2() { return true; }
 
template<C2<0> T> struct S1; // 错误: <wildcard, 0> 不匹配 <typename T> 或 <int T>
template<C2 T> struct S2; // #1 与 #2 均匹配:错误
(概念 TS)

[编辑] 制约的偏序

在任何进一步分析前,通过替换每个具名制约的体和每个 requires 表达式正规化制约,直到剩下原子制约上的合取与析取序列。原子制约是谓词制约、表达式制约、隐式转换制约、实参推导制约和异常制约。

若能不因等价性分析类型和表达式就能证明概念 P 蕴含 概念 Q ,则说 P 包含 Q (故 N >= 0 不包含 N > 0

特别是,首个 P 被转换成析取范式且 Q 被转换成合取范式,且以下列方式比较它们:

  • 每个原子制约 A 包含等价的原子制约 A
  • 每个原子制约 A 包含合取 A&&B 且不包含析取 A||B
  • 每个析取 A||B 包含 A ,但合取 A&&B 不包含 A

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

若声明 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 ,但不通过另外方式
// RevIterator 比 Decrementable 更受制约
 
void f(Decrementable); // #1
void f(RevIterator);   // #2
 
f(0);       // int 仅满足可自增,选择 #1
f((int*)0); // int* 满足二个制约,选择 #2 ,因为更受制约
 
void g(auto);          // #3(无制约)
void g(Decrementable); // #4
 
g(true);  // bool 不满足 Decrementable ,选择 #3
g(0);     // int 满足 Decrementable ,选择 #4 因为它更受制约

[编辑] 关键词

concept, requires

[编辑] 编译器支持

GCC >= 6.1 支持此页面文本中标记为(概念 TS)的概念 TS (要求选项 -fconcepts )。