命名空间
变体
操作

约束与概念

来自 cppreference.cn
< cpp‎ | 实验性


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

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

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

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

#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

概念的目的是建模语义类别(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'); // 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-表达式(见下文)中,也可以直接作为概念的主体。

有 9 种类型的约束:

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

前三种类型的约束可以直接作为概念的主体或作为临时requires子句出现。

template<typename T>
requires // requires-clause (ad-hoc constraint)
sizeof(T) > 1 && get_value<T>() // conjunction of two predicate constraints
void f(T);

当多个约束附加到同一声明时,总约束是按以下顺序排列的合取:由模板引入引入的约束,每个模板参数按出现顺序的约束,模板参数列表后的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

[编辑] 合取

约束 PQ 的合取指定为 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&& 重载不允许在约束合取中使用。

[编辑] 析取

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

两个约束的析取在任一约束满足时满足。析取从左到右评估并短路(如果左侧约束满足,则不尝试将模板参数推导到右侧约束中)。用户定义的 operator|| 重载不允许在约束析取中使用。

// example constraint from the standard library (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>{} // bad predicate constraint: S<T>{} is not bool
void f(T);
f(0); // error: constraint never satisfied

[编辑] 要求

关键字 requires 有两种用法:

1) 引入requires-子句,它指定对模板参数或函数声明的约束。
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-表达式
2) 开始一个requires-表达式,它是一个 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-表达式的语法如下:

requires ( parameter-list(可选) ) { requirement-seq }
parameter-list - 一个逗号分隔的参数列表,类似于函数声明,但默认参数不允许,最后一个参数也不能是省略号。这些参数没有存储、链接或生命周期。这些参数在 requirement-seq 的闭合 } 之前都在作用域内。如果没有使用参数,圆括号也可以省略。
requirement-seq - 由空格分隔的要求序列,如下所述(每个要求以分号结尾)。每个要求都为该requires-表达式定义的约束合取添加另一个约束。

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

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

要求可以引用作用域内的模板参数和 parameter-list 中引入的局部参数。当参数化时,requires-表达式被认为引入了一个参数化约束

将模板参数替换到requires-表达式中可能会导致其要求中形成无效类型或表达式。在这种情况下:

  • 如果发生在 模板实体 声明之外的requires-表达式中发生替换失败,则程序格式错误。
  • 如果requires-表达式用于 模板实体 的声明中,则相应的约束被视为“不满足”,且替换失败不是错误,然而,
  • 如果对于每个可能的模板参数,requires-表达式都会发生替换失败,则程序格式错误,无需诊断。
template<class T> concept bool C = requires {
    new int[-(int)sizeof(T)]; // invalid for every T: ill-formed, no diagnostic required
};

[编辑] 简单要求

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

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

[编辑] 类型要求

类型要求是关键字 typename 后跟一个类型名称,可选地带限定符。要求是命名类型存在(一个类型约束):这可以用于验证某个命名的嵌套类型是否存在,或者类模板特化是否命名一个类型,或者别名模板是否命名一个类型。

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

[编辑] 复合要求

复合要求具有以下形式:

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

[编辑] 嵌套要求

嵌套要求是另一个以分号结尾的requires-子句。这用于引入谓词约束(见上文),它们以应用于局部参数的其他命名概念表示(在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
};

[编辑] 概念解析

像任何其他函数模板一样,函数概念(但不是变量概念)可以被重载:可以提供多个概念定义,它们都使用相同的 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); // the set of concepts referred to by C includes both #1 and #2;
           // concept resolution (see below) selects #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) 如果概念作为模板-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>(); // 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

[编辑] 约束的偏序

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

如果可以证明 P 蕴含 Q,而无需分析类型和表达式的等价性(因此 N >= 0 不蕴含 N > 0),则概念 P 被认为包含概念 Q

具体来说,首先将 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 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

[编辑] 关键字

concept, requires

[编辑] 编译器支持

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