命名空间
变体
操作

约束和概念

来自 cppreference.com


此页面描述了一个实验性的核心语言特性。对于在标准库规范中使用的命名类型要求,请参见 命名要求

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

约束还可用于将变量声明和函数返回类型中的自动类型推断限制为仅满足指定要求的类型。

这类要求的命名集合称为概念。每个概念都是一个谓词,在编译时进行评估,并在其用作约束的模板的接口中成为一部分

#include <string>
#include <locale>
using namespace std::literals;
 
// Declaration of the concept "EqualityComparable", which is satisfied by
// any type T such that for values a and b of type T,
// the expression a==b compiles and its result is convertible to bool
template<typename T>
concept bool EqualityComparable = requires(T a, T b) {
    { a == b } -> bool;
};
 
void f(EqualityComparable&&); // declaration of a constrained function template
// template<typename T>
// void f(T&&) requires EqualityComparable<T>; // long form of the same
 
int main() {
  f("abc"s); // OK, std::string is EqualityComparable
  f(std::use_facet<std::ctype<char>>(std::locale{})); // Error: not EqualityComparable 
}

约束违规在编译时被检测到,在模板实例化过程的早期,这将导致易于理解的错误消息。

std::list<int> l = {3,-1,10};
std::sort(l.begin(), l.end()); 
//Typical compiler diagnostic without concepts:
//  invalid operands to binary expression ('std::_List_iterator<int>' and
//  'std::_List_iterator<int>')
//                           std::__lg(__last - __first) * 2);
//                                     ~~~~~~ ^ ~~~~~~~
// ... 50 lines of output ...
//
//Typical compiler diagnostic with concepts:
//  error: cannot call std::sort with std::_List_iterator<int>
//  note:  concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied

概念的目的是对语义类别(数字、范围、正则函数)进行建模,而不是对语法限制(有加号、数组)进行建模。根据 ISO C++ 核心准则 T.20,“指定有意义语义的能力是真正概念的定义特征,而不是语法约束。”

如果支持特性测试,则此处描述的特性由宏常量 __cpp_concepts 表示,其值为等于或大于 201507

内容

[编辑] 占位符

无约束占位符 auto约束占位符(其形式为 concept-name < template-argument-list(可选)>)是将要推断的类型的占位符。

占位符可以出现在变量声明中(在这种情况下,它们是从初始化程序推断的)或函数返回类型中(在这种情况下,它们是从 return 语句推断的)

std::pair<auto, auto> p2 = std::make_pair(0, 'a'); // first auto is int,
                                                   // second auto is char
 
Sortable x = f(y); // the type of x is deduced from the return type of f, 
                   // only compiles if the type satisfies the constraint Sortable
 
auto f(Container) -> Sortable; // return type is deduced from the return statement
                               // only compiles if the type satisfies Sortable

占位符也可以出现在参数中,在这种情况下,它们将函数声明转换为模板声明(如果占位符受约束,则为约束的)

void f(std::pair<auto, EqualityComparable>); // this is a template with two parameters:
       // unconstrained type parameter and a constrained non-type parameter

约束占位符可以在 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; // error, invalid use of a non-type concept
void f(std::array<auto, Even>); // error, invalid use of a non-type concept
template<Even N> void f(std::array<auto, N>); // OK

[编辑] 简写模板

如果函数参数列表中出现一个或多个占位符,则函数声明实际上是函数模板声明,其模板参数列表包括每个唯一占位符的模板参数(按照出现的顺序)

// short form
void g1(const EqualityComparable*, Incrementable&);
// long form:
// template<EqualityComparable T, Incrementable U> void g1(const T*, U&);
// longer form:
// template<typename T, typename U>
// void g1(const T*, U&) requires EqualityComparable<T> && Incrementable<U>;
 
void f2(std::vector<auto*>...);
// long form: template<typename... T> void f2(std::vector<T*>...);
 
void f4(auto (auto::*)(auto));
// long form: template<typename T, typename U, typename V> void f4(T (U::*)(V));

由等效约束类型说明符引入的所有占位符都具有相同的模板参数。但是,每个无约束说明符(auto)始终引入不同的模板参数

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

函数模板和类模板都可以使用模板介绍来声明,模板介绍的语法为 concept-name { parameter-list(可选)} ,在这种情况下,不需要关键字 template:模板介绍的 parameter-list 中的每个参数都将成为一个模板参数,其种类(类型、非类型、模板)由命名概念中相应参数的种类决定。

除了声明模板之外,模板介绍还将关联一个谓词约束(见下文),该约束为(变量概念)命名或调用(函数概念)介绍中命名的概念。

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

对于函数模板,模板介绍可以与占位符结合使用

Sortable{T} void f(T, auto);
// long form: template<Sortable T, typename U> void f(T, U);
// alternative using only placeholders: void f(Sortable, auto);

[编辑] 概念

概念是一组命名要求。概念的定义出现在命名空间范围内,并具有 函数模板 定义(在这种情况下称为函数概念)或 变量模板 定义(在这种情况下称为变量概念)的形式。唯一的区别是关键字 concept 出现在 decl-specifier-seq

// variable concept from the standard library (Ranges TS)
template <class T, class U>
concept bool Derived = std::is_base_of<U, T>::value;
 
// function concept from the standard library (Ranges TS)
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>(); } // error
template<typename T>
concept bool V = V<T*>; // error

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

[编辑] 约束

约束是一系列逻辑运算,用于指定模板参数的要求。它们可以出现在 *requires-expression* 中(见下文),也可以直接作为概念的主体。

有 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-clause (ad-hoc constraint)
sizeof(T) > 1 && get_value<T>() // conjunction of two predicate constraints
void f(T);

当多个约束附加到同一个声明时,总约束是以下顺序的联合:由 *template introduction* 引入的约束、每个模板参数按出现顺序的约束、模板参数列表后的 *requires* 语句、每个函数参数按出现顺序的约束、尾随的 *requires* 语句。

// the declarations declare the same constrained function template 
// with the constraint Incrementable<T> && Decrementable<T>
template<Incrementable T> void f(T) requires Decrementable<T>;
template<typename T> requires Incrementable<T> && Decrementable<T> void f(T); // ok
 
// the following two declarations have different constraints:
// the first declaration has Incrementable<T> && Decrementable<T>
// the second declaration has Decrementable<T> && Incrementable<T>
// Even though they are logically equivalent.
// The second declaration is ill-formed, no diagnostic required
 
template<Incrementable T> requires Decrementable<T> void g();
template<Decrementable T> requires Incrementable<T> void g(); // error

[edit] 联合

约束 `P` 和 `Q` 的联合指定为 P && Q.

// example concepts from the standard library (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&&` 的重载。

[edit] 析取

约束 `P` 和 `Q` 的析取指定为 P || Q.

如果两个约束中的任何一个约束都满足,则两个约束的析取就满足。析取从左到右进行评估,并进行短路(如果左侧约束满足,则不会尝试将模板参数代入右侧约束)。在约束析取中不允许用户定义的 `operator||` 的重载。

// example constraint from the standard library (Ranges TS)
template <class T = void>
requires EqualityComparable<T>() || Same<T, void>
struct equal_to;

[edit] 谓词约束

谓词约束是类型为 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>{} // bad predicate constraint: S<T>{} is not bool
void f(T);
f(0); // error: constraint never satisfied

[edit] 要求

关键字 requires 以两种方式使用。

1) 用于引入 *requires-clause*,它指定模板参数或函数声明的约束。
template<typename T>
void f(T&&) requires Eq<T>; // can appear as the last element of a function declarator
 
template<typename T> requires Addable<T> // or right after a template parameter list
T add(T a, T b) { return a + b; }
在这种情况下,关键字 *requires* 后面必须跟一些常量表达式(因此可以写 "requires true;"),但意图是使用命名概念(如上面的示例)或命名概念的联合/析取或 *requires-expression*。
2) 用于开始 *requires-expression*,它是一个类型为 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(optional) ) { requirement-seq }
parameter-list - 一个逗号分隔的参数列表,就像函数声明中的参数列表一样,只是不允许默认参数,最后一个参数也不能是省略号。这些参数没有存储、链接或生命周期。这些参数在 requirement-seq 的结束括号 `} ` 之前都在作用域内。如果未使用参数,则也可以省略圆括号。
requirement-seq - 由空格分隔的 *requirement* 序列,在下面描述(每个 requirement 以分号结尾)。每个 requirement 都为 *requires-expression* 定义的约束 *联合* 添加另一个约束。

requirements-seq 中的每个 requirement 都是以下之一。

  • 简单 requirement
  • 类型 requirement
  • 复合 requirement
  • 嵌套 requirement

Requirement 可以引用作用域内的模板参数以及在 parameter-list 中引入的局部参数。当参数化时,requires-expression 被认为是引入了一个 *参数化约束*。

将模板参数代入 requires-expression 可能导致在它的 requirement 中形成无效的类型或表达式。在这种情况下,

  • 如果在 *requires-expression* 中出现代入失败,而 *requires-expression* 是在 *templated entity* 声明之外使用的,则程序格式错误。
  • 如果 *requires-expression* 是在 *templated entity* 的声明中使用的,则相应的约束将被视为“不满足”,而 *substitution failure* 不会是错误,但是
  • 如果对于所有可能的模板参数,*requires-expression* 都会出现代入失败,则程序格式错误,不需要诊断。
template<class T> concept bool C = requires {
    new int[-(int)sizeof(T)]; // invalid for every T: ill-formed, no diagnostic required
};

[edit] 简单 requirement

简单 requirement 是一个任意的表达式语句。requirement 是表达式有效(这是一个 *表达式约束*)。与谓词约束不同,不会进行求值,只会检查语言是否正确。

template<typename T>
concept bool Addable =
requires (T a, T b) {
    a + b; // "the expression a+b is a valid expression that will compile"
};
 
// example constraint from the standard library (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));
};

[edit] 类型 requirement

类型 requirement 是关键字 typename 后面跟着一个类型名称,可选地进行限定。requirement 是指定的类型存在(一个 *类型约束*):这可以用来验证某个命名的嵌套类型是否存在,或者类模板特化是否命名了一个类型,或者别名模板是否命名了一个类型。

template<typename T> using Ref = T&;
template<typename T> concept bool C =
requires {
    typename T::inner; // required nested member name
    typename S<T>;     // required class template specialization
    typename Ref<T>;   // required alias template substitution
};
 
//Example concept from the standard library (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> is valid and names a type
    { CommonType<T, U>{std::forward<T>(t)} }; 
    { CommonType<T, U>{std::forward<U>(u)} }; 
};

[edit] 复合 requirement

复合 requirement 的形式为

{ expression } noexcept(optional) trailing-return-type(optional) ;

并指定以下约束的联合。

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; // the expression *x must be valid
                               // AND the type T::inner must be valid
                               // AND the result of *x must be convertible to T::inner
};
 
// Example concept from the standard library (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) }; // direct initialization constraint has to use expression
    { !b1 } -> bool; // compound constraint
    requires Same<decltype(b1 && b2), bool>; // nested constraint, see below
    requires Same<decltype(b1 || b2), bool>;
};

[edit] 嵌套 requirement

嵌套 requirement 是另一个 *requires-clause*,以分号结尾。这用于引入 *谓词约束*(见上文),这些约束是用应用于局部参数的其他命名概念来表达的(在 requires 语句之外,谓词约束不能使用参数,并且将表达式直接放在 requires 语句中会使它成为表达式约束,这意味着它不会被求值)。

// example constraint from 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)>;  // nested: "Same<...> evaluates to true"
    { a.~T() } noexcept;  // compound: "a.~T()" is a valid expression that doesn't throw
    requires Same<T*, decltype(new T)>; // nested: "Same<...> evaluates to true"
    requires Same<T*, decltype(new T[n])>; // nested
    { delete new T };  // compound
    { delete new T[n] }; // compound
};

[edit] 概念解析

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

concept-name(可能被限定)出现在以下位置时,就会执行概念解析。

1) 受约束的类型说明符 void f(Concept); std::vector<Concept> x = ...;
2) 受约束的参数 template<Concept T> void f();
3) 模板引入 Concept{T} struct X;
4) *constraint-expression* 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); // the set of concepts referred to by C includes both #1 and #2;
           // concept resolution (see below) selects #1.

为了执行概念解析,将每个与名称(以及可选的限定符)匹配的概念的 *template parameters* 与一系列 *concept arguments* 进行匹配,这些参数是模板参数和 *wildcards*。通配符可以匹配任何类型的模板参数(类型、非类型、模板)。参数集的构造方式不同,具体取决于上下文。

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>(); // matches #1

概念解析通过将每个参数与每个可见概念的对应参数进行匹配来执行。默认模板参数(如果使用)将为每个不对应于参数的参数实例化,然后附加到参数列表中。模板参数仅当它具有相同的类型(类型、非类型、模板)时才与参数匹配,除非参数是通配符。参数包匹配零个或多个参数,只要所有参数在类型方面匹配模式(除非它们是通配符)。

如果任何参数与其对应参数不匹配,或者参数多于参数并且最后一个参数不是包,则概念不可行。如果存在零个或多个可行的概念,则程序格式错误。

template<typename T> concept bool C2() { return true; }
template<int T> concept bool C2() { return true; }
 
template<C2<0> T> struct S1; // error: <wildcard, 0> matches 
                             // neither <typename T> nor <int T>
template<C2 T> struct S2; // both #1 and #2 match: error

[edit] 约束的偏序

在进行任何进一步的分析之前,约束通过替换每个名称概念和每个 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 subsumes Decrementable, but not the other way around
// RevIterator is more constrained as Decrementable
 
void f(Decrementable); // #1
void f(RevIterator);   // #2
 
f(0);       // int only satisfies Decrementable, selects #1
f((int*)0); // int* satisfies both constraints, selects #2 as more constrained
 
void g(auto);          // #3 (unconstrained)
void g(Decrementable); // #4
 
g(true);  // bool does not satisfy Decrementable, selects #3
g(0);     // int satisfies Decrementable, selects #4 because it is more constrained

[edit] 关键词

conceptrequires

[edit] 编译器支持

GCC >= 6.1 支持此技术规范(需要选项 -fconcepts)