Namespaces
Variants

Constraints and concepts

From cppreference.net


本页描述了一项实验性的核心语言特性。关于标准库规范中使用的命名类型要求,请参阅 命名要求

类模板 函数模板 以及非模板函数(通常是类模板的成员)可以与 约束 相关联,该约束规定了模板参数的要求,可用于选择最合适的函数重载和模板特化。

约束也可用于限制变量声明和函数返回类型中的自动类型推导,使其仅适用于满足指定要求的类型。

这类需求的命名集合被称为 概念 。每个概念都是一个在编译时求值的谓词,并作为约束条件成为使用该概念的模板接口的一部分:

#include <string>
#include <locale>
using namespace std::literals;
// 概念 "EqualityComparable" 的声明,满足该概念的类型 T 需满足:
// 对于类型 T 的值 a 和 b,表达式 a==b 能够编译且其结果可转换为 bool
template<typename T>
concept bool EqualityComparable = requires(T a, T b) {
    { a == b } -> bool;
};
void f(EqualityComparable&&); // 受约束函数模板的声明
// template<typename T>
// void f(T&&) requires EqualityComparable<T>; // 等价的长形式声明
int main() {
  f("abc"s); // 正确: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 ,“能够定义有意义的语义是真正概念(而非语法约束)的决定性特征。”

若支持功能测试,此处描述的功能由宏常量 __cpp_concepts 指示,其值等于或大于 201507

目录

占位符

无约束占位符 auto 以及具有 concept-name < template-argument-list (可选) > 形式的 约束占位符 ,都是用于推导类型的占位符。

占位符可能出现在变量声明中(此时从初始化器推导类型)或函数返回类型中(此时从返回语句推导类型)

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 约束时才能编译通过

占位符也可能出现在参数中,这种情况下它们会将函数声明转变为模板声明(当占位符受约束时则形成受约束模板声明)

void f(std::pair<auto, EqualityComparable>); // 这是一个包含两个参数的模板:
       // 无约束的类型参数和受约束的非类型参数

受约束占位符可用于任何可以使用 auto 的地方,例如在泛型lambda声明中

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

如果受约束类型说明符指定了非类型或模板,但被用作受约束占位符,则程序非良构:

template<size_t N> concept bool Even = (N%2 == 0);
struct S1 { int n; };
int Even::* p2 = &S1::n; // 错误:非法使用非类型概念
void f(std::array<auto, Even>); // 错误:非法使用非类型概念
template<Even N> void f(std::array<auto, N>); // 正确

简写模板

如果一个或多个占位符出现在函数参数列表中,该函数声明实际上是一个函数模板声明,其模板参数列表按出现顺序为每个唯一占位符包含一个自动生成的参数

// 简写形式
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);

函数模板和类模板都可以使用 模板引导 进行声明,其语法为 概念名称 { 参数列表 (可选) } ,这种情况下不需要使用关键字 template :模板引导的 参数列表 中的每个参数都会成为一个模板参数,其种类(类型、非类型、模板)由命名概念中对应参数的种类决定。

除了声明模板外,模板引入还关联一个 谓词约束 (见下文),该约束会命名(针对变量概念)或调用(针对函数概念)由引入命名的概念。

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

概念

概念(concept)是具名要求集合。概念的定义出现在命名空间作用域,其形式为 函数模板 定义(此时称为 函数概念 )或 变量模板 定义(此时称为 变量概念 )。唯一区别在于关键字 concept 出现在 decl-specifier-seq 中:

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

以下限制适用于函数概念:

  • 不允许使用 inline constexpr ,函数自动为 inline constexpr
  • 不允许使用 friend virtual
  • 不允许异常规范,函数自动为 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*>; // 错误

不允许对概念进行显式实例化、显式特化或部分特化(不能更改约束原始定义的含义)

约束条件

约束是一系列逻辑操作的序列,用于规定模板参数必须满足的要求。它们可以出现在 requires表达式 (详见下文)中,也可直接作为概念的主体

共有9种约束类型:

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

前三种约束类型可以直接作为概念的主体出现,或作为临时 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); // 正确
// 以下两个声明具有不同的约束条件:
// 第一个声明的约束为 Incrementable<T> && Decrementable<T>
// 第二个声明的约束为 Decrementable<T> && Incrementable<T>
// 尽管它们在逻辑上是等价的
// 第二个声明属于病式构造,不要求编译器给出诊断信息
template<Incrementable T> requires Decrementable<T> void g();
template<Decrementable T> requires Incrementable<T> void g(); // 错误

合取约束

约束条件 P Q 的合取关系表示为 P && Q

// 来自标准库的概念示例(Ranges TS)
template <class T>
concept bool Integral = std::is_integral<T>::value;
template <class T>
concept bool SignedIntegral = Integral<T> && std::is_signed<T>::value;
template <class T>
concept bool UnsignedIntegral = Integral<T> && !SignedIntegral<T>;

两个约束的合取仅当两个约束均被满足时才成立。合取从左向右求值且具有短路效应(若左侧约束不满足,则不会尝试对右侧约束进行模板实参替换:这可以防止因非即时上下文中的替换导致的失败)。约束合取中不允许使用用户自定义的 operator&& 重载。

析取

约束条件 P Q 的析取关系表示为 P || Q

两个约束的析取关系在任一约束满足时即成立。析取从左向右求值并采用短路求值(若左侧约束已满足,则不会尝试对右侧约束进行模板实参推导)。约束析取中不允许使用用户自定义的 operator|| 重载。

// 来自标准库的示例约束 (Ranges TS)
template <class T = void>
requires EqualityComparable<T>() || Same<T, void>
struct equal_to;

谓词约束

谓词约束是一个类型为 bool 的常量表达式。仅当其求值结果为 true 时才得到满足

template<typename T> concept bool 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>{} 不是布尔类型
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-expression
2) 用于开始一个 要求表达式 ,这是一个类型为 bool 的纯右值表达式,用于描述某些模板参数的约束。当对应概念得到满足时该表达式为 true ,否则为 false:
template<typename T>
concept bool Addable = requires (T x) { x + x; }; // requires-expression
template<typename T> requires Addable<T> // requires-clause, not requires-expression
T add(T a, T b) { return a + b; }
template<typename T>
requires requires (T x) { x + x; } // ad-hoc constraint, note keyword used twice
T add(T a, T b) { return a + b; }

requires-expression 的语法如下:

requires ( 参数列表 (可选) ) { 要求序列 }
parameter-list - 逗号分隔的参数列表,类似于函数声明中的形式,但不允许默认参数且最后一个参数不能是省略号。这些参数没有存储空间、链接性或生存期。这些参数的作用域持续到 requirement-seq 的闭合 } 为止。如果未使用任何参数,圆括号也可以省略
requirement-seq - 以空白符分隔的 需求 序列(每个需求以分号结尾)。每个需求都会向该requires表达式定义的约束合取式添加另一个约束。

每个在 requirements-seq 中的要求均为以下之一:

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

要求可以引用在作用域内的模板参数以及在 参数列表 中引入的局部参数。当参数化时,requires表达式被称为引入了 参数化约束

模板参数代入 requires-expression 时可能导致其要求中形成无效类型或表达式。在此情况下,

  • 如果在 模板化实体 声明外部使用的requires表达式中发生替换失败,则程序非良构。
  • 如果requires表达式用于 模板化实体 的声明中,则相应约束被视为"未满足"且 替换失败不是错误 ,然而
  • 如果对于所有可能的模板参数,requires表达式中都会发生替换失败,则程序非良构,不要求诊断:
template<class T> concept bool C = requires {
    new int[-(int)sizeof(T)]; // 对所有 T 均无效:格式错误,不要求诊断
};

简单要求

一个简单需求是一个任意表达式语句。该需求要求表达式是有效的(这是一个 表达式约束 )。与谓词约束不同,这里不会进行求值运算,仅检查语言正确性。

template<typename T>
concept bool Addable =
requires (T a, T b) {
    a + b; // "表达式 a+b 是能够编译的有效表达式"
};
// 来自标准库的示例约束(ranges TS)
template <class T, class U = T>
concept bool 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 bool C =
requires {
    typename T::inner; // 需要嵌套成员名称
    typename S<T>;     // 需要类模板特化
    typename Ref<T>;   // 需要别名模板替换
};
// 标准库中的概念示例(Ranges TS)
template <class T, class U> using CommonType = std::common_type_t<T, U>;
template <class T, class U> concept bool 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) expression 是一个有效的表达式( 表达式约束
2) 若使用 noexcept ,表达式也必须为 noexcept( 异常约束
3) trailing-return-type 命名的类型使用了占位符,则该类型必须可从表达式类型推导得出( 实参推导约束
4) trailing-return-type 命名的类型未使用占位符,则需额外添加两个约束:
4a) trailing-return-type 命名的类型是有效的( 类型约束
4b) 表达式的结果可 隐式转换 为该类型( 隐式转换约束
template<typename T> concept bool C2 =
requires(T x) {
    {*x} -> typename T::inner; // 表达式 *x 必须合法
                               // 且类型 T::inner 必须合法
                               // 且 *x 的结果必须可转换为 T::inner
};
// 标准库概念示例(Ranges TS)
template <class T, class U> concept bool Same = std::is_same<T,U>::value;
template <class B> concept bool 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子句中会使其成为表达式约束,这意味着它不会被求值)

// 来自 Ranges TS 的示例约束
template <class T>
concept bool 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] }; // 复合要求
};

概念解析

与其他函数模板类似,函数概念(但不包括变量概念)支持重载:可以提供多个使用相同 概念名称 的概念定义。

概念解析在以下情况执行:当 概念名称 (可以是限定名称)出现在

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> matches <T>, selects #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> matches <T, U>, selects #2
3) 若概念出现在模板引导中,实参列表是与模板引导中形参列表等长的一系列占位符
template<typename... Ts>
concept bool C3 = true;
C3{T} void q2();     // OK: <T> matches <...Ts>
C3{...Ts} void q1(); // OK: <...Ts> matches <...Ts>
4) 若概念作为模板特化名称出现,则概念实参列表即为该模板特化的实参序列
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; // 错误:<通配符, 0> 无法匹配
                             // <typename T> 或 <int T>
template<C2 T> struct S2; // #1 和 #2 同时匹配:错误

约束的部分排序

在进行任何进一步分析之前,约束会通过替换每个命名概念和每个requires表达式的主体来进行 规范化 ,直到剩下的是原子约束的合取与析取序列,这些原子约束包括谓词约束、表达式约束、类型约束、隐式转换约束、实参推导约束和异常约束。

若概念 P 可被证明 蕴含 Q ,且无需分析类型与表达式的等价性(因此 N >= 0 不包含 N > 0 ),则称概念 P 包含 概念 Q

具体来说,首先将 P 转换为析取范式,并将 Q 转换为合取范式,然后按如下方式进行比较:

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

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

如果声明 D1 D2 均受约束,且 D1 的规范化约束包含 D2 的规范化约束(或者若 D1 受约束而 D2 无约束),则称 D1 至少与 D2 受约束程度相同。若 D1 至少与 D2 受约束程度相同,而 D2 不至少与 D1 受约束程度相同,则称 D1 D2 更受约束。

template<typename T>
concept bool Decrementable = requires(T t) { --t; };
template<typename T>
concept bool RevIterator = Decrementable<T> && requires(T t) { *t; };
// RevIterator 包含 Decrementable,但反之不成立
// RevIterator 是比 Decrementable 约束性更强的概念
void f(Decrementable); // #1
void f(RevIterator);   // #2
f(0);       // int 仅满足 Decrementable,选择 #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 支持此技术规范(需要选项 - fconcepts