命名空间
变体
操作

约束与概念 (自 C++20 起)

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句(循环)
for
range-for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (直到 C++17*)
noexcept 说明符 (C++11)
异常
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 
 
 
 

类模板、函数模板(包括泛型 lambda 表达式)以及其他模板化函数(通常是类模板的成员)可能与一个约束相关联,该约束指定了对模板参数的要求,可用于选择最合适的函数重载和模板特化。

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

#include <cstddef>
#include <concepts>
#include <functional>
#include <string>
 
// Declaration of the concept “Hashable”, which is satisfied by any type “T”
// such that for values “a” of type “T”, the expression std::hash<T>{}(a)
// compiles and its result is convertible to std::size_t
template<typename T>
concept Hashable = requires(T a)
{
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
 
struct meow {};
 
// Constrained C++20 function template:
template<Hashable T>
void f(T) {}
//
// Alternative ways to apply the same constraint:
// template<typename T>
//     requires Hashable<T>
// void f(T) {}
//
// template<typename T>
// void f(T) requires Hashable<T> {}
//
// void f(Hashable auto /* parameter-name */) {}
 
int main()
{
    using std::operator""s;
 
    f("abc"s);    // OK, std::string satisfies Hashable
    // f(meow{}); // Error: meow does not satisfy Hashable
}

对约束的违反会在编译时,即模板实例化过程的早期被检测到,这会产生易于理解的错误消息。

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,“指定有意义语义的能力是真正概念的定义性特征,而不是语法约束。”

目录

[编辑] 概念

概念是一组命名的要求。概念的定义必须出现在命名空间作用域内。

概念的定义形式如下:

template < template-parameter-list >

concept concept-name attr (可选) = constraint-expression;

属性 - 任意数量的属性序列
// concept
template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;

概念不能递归地引用自身,也不能被约束。

template<typename T>
concept V = V<T*>; // error: recursive concept
 
template<class T>
concept C1 = true;
template<C1 T>
concept Error1 = true; // Error: C1 T attempts to constrain a concept definition
template<class T> requires C1<T>
concept Error2 = true; // Error: the requires clause attempts to constrain a concept

不允许概念的显式实例化、显式特化或部分特化(原始约束定义的含义不能被改变)。

概念可以在 id-expression 中命名。如果约束表达式被满足,id-expression 的值为 true,否则为 false

概念也可以在类型约束中命名,作为以下的一部分:

类型约束中,概念的模板参数比其参数列表要求的少一个,因为上下文中推导的类型被隐式地用作概念的第一个参数。

template<class T, class U>
concept Derived = std::is_base_of<U, T>::value;
 
template<Derived<Base> T>
void f(T); // T is constrained by Derived<T, Base>

[编辑] 约束

约束是逻辑操作和操作数的序列,它指定了对模板参数的要求。它们可以出现在requires 表达式中,或直接作为概念的主体。

约束有三种(直到 C++26)四种(自 C++26 起)类型:

1) 合取
2) 析取
3) 原子约束
4) 折叠展开的约束
(C++26 起)

与声明关联的约束通过规范化逻辑 AND 表达式来确定,其操作数按以下顺序排列:

  1. 为每个约束的类型模板参数或用约束占位符类型声明的非类型模板参数引入的约束表达式,按出现顺序;
  2. 模板参数列表后面的requires 子句中的约束表达式;
  3. 缩写函数模板声明中,为每个具有受限占位符类型的参数引入的约束表达式;
  4. 尾随requires 子句中的约束表达式。

这个顺序决定了在检查满足性时实例化约束的顺序。

[编辑] 重声明

受约束的声明只能使用相同的语法形式重新声明。不需要诊断。

// These first two declarations of f are fine
template<Incrementable T>
void f(T) requires Decrementable<T>;
 
template<Incrementable T>
void f(T) requires Decrementable<T>; // OK, redeclaration
 
// Inclusion of this third, logically-equivalent-but-syntactically-different
// declaration of f is ill-formed, no diagnostic required
template<typename T>
    requires Incrementable<T> && Decrementable<T>
void f(T);
 
// 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.
 
template<Incrementable T> 
void g(T) requires Decrementable<T>;
 
template<Decrementable T> 
void g(T) requires Incrementable<T>; // ill-formed, no diagnostic required

[编辑] 合取

两个约束的合取通过在约束表达式中使用 && 运算符形成。

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, calls #2. When checking the constraints of #1,
            // 'sizeof(char) > 1' is not satisfied, so get_value<T>() is not checked
}

[编辑] 析取

两个约束的析取通过在约束表达式中使用 || 运算符形成。

两个约束的析取如果其中任一约束满足则满足。析取从左到右评估并短路(如果左约束满足,则不尝试将模板参数替换到右约束中)。

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

[编辑] 原子约束

原子约束由表达式 E 以及从出现在 E 中的模板参数到涉及受约束实体模板参数的模板参数的映射(称为参数映射)组成。

原子约束在约束规范化期间形成。E 从不是逻辑 AND 或逻辑 OR 表达式(它们分别形成合取和析取)。

原子约束的满足性通过将参数映射和模板参数替换到表达式 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); // error: S<int>{} does not have type bool when checking #1,
          // even though #2 is a better match
}

如果两个原子约束在源代码级别由相同的表达式形成,并且它们的参数映射等效,则认为它们是相同的

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); // error, ambiguous:
           // the is_meowable<T> in Meowable and BadMeowableCat forms distinct atomic
           // constraints that are not identical (and so do not subsume each other)
 
    f2(0); // OK, calls #4, more constrained than #3
           // GoodMeowableCat got its is_meowable<T> from Meowable
}

折叠展开的约束

一个折叠展开的约束由约束 C 和折叠运算符(&&||)构成。一个折叠展开的约束是一个包展开

N 为包展开参数中的元素数量。

  • 如果包展开无效(例如展开大小不同的包),则折叠展开的约束不满足。
  • 如果 N0,则如果折叠运算符是 &&,则折叠展开的约束满足;如果折叠运算符是 ||,则不满足。
  • 对于一个 N 为正数的折叠展开约束,对于 [1N] 中的每个 i,每个包展开参数都替换为相应的第 i 个元素,按递增顺序。
  • 对于折叠运算符为 && 的折叠展开约束,如果替换第 j 个元素违反了 C,则折叠展开的约束不满足。在这种情况下,对于任何大于 ji,都不进行替换。否则,折叠展开的约束满足。
  • 对于折叠运算符为 || 的折叠展开约束,如果替换第 j 个元素满足 C,则折叠展开的约束满足。在这种情况下,对于任何大于 ji,都不进行替换。否则,折叠展开的约束不满足。


template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;
 
// in C++23, these two overloads of g() have distinct atomic constraints 
// that are not identical and so do not subsume each other: calls to g() are ambiguous
// in C++26, the folds are expanded and constraint on overload #2 (both move and copy
// required), subsumes constraint on overload #1 (just the move is required)
template <class... T>
requires (A<T> && ...) void g(T...); // #1
 
template <class... T>
requires (C<T> && ...) void g(T...); // #2


(C++26 起)

[编辑] 约束规范化

约束规范化是将约束表达式转换为原子约束的合取和析取序列的过程。表达式的范式定义如下:

  • 表达式 (E) 的范式是 E 的范式。
  • 表达式 E1 && E2 的范式是 E1E2 的范式的合取。
  • 表达式 E1 || E2 的范式是 E1E2 的范式的析取。
  • 表达式 C<A1, A2, ... , AN> 的范式,其中 C 命名一个概念,是 C 的约束表达式的范式,替换 C 的每个原子约束的参数映射中 C 的相应模板参数为 A1, A2, ..., AN。如果这样的替换到参数映射中导致无效类型或表达式,则程序格式错误,不需要诊断。
template<typename T>
concept A = T::value || true;
 
template<typename U>
concept B = A<U*>; // OK: normalized to the disjunction of 
                   // - T::value (with mapping T -> U*) and
                   // - true (with an empty mapping).
                   // No invalid type in mapping even though
                   // T::value is ill-formed for all pointer types
 
template<typename V>
concept C = B<V&>; // Normalizes to the disjunction of
                   // - T::value (with mapping T-> V&*) and
                   // - true (with an empty mapping).
                   // Invalid type V&* formed in mapping => ill-formed NDR
  • 表达式 (E && ...)(... && E) 的范式是一个折叠展开的约束,其中 CE 的范式,折叠运算符是 &&
  • 表达式 (E || ...)(... || E) 的范式是一个折叠展开的约束,其中 CE 的范式,折叠运算符是 ||
  • 表达式 (E1 && ... && E2)(E1 || ... || E2) 的范式分别是:
  • (E1 && ...) && E2(E1 || ...) || E2,如果 E1 包含未展开的包,或者
  • E1 && (... && E2)E1 || (... || E2),否则。
(C++26 起)
  • 任何其他表达式 E 的范式是其表达式为 E 且参数映射为恒等映射的原子约束。这包括所有折叠表达式,即使是那些折叠 &&|| 运算符的表达式。

用户定义的 &&|| 重载对约束规范化没有影响。

[编辑] requires 子句

关键词 requires 用于引入一个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 表达式

该表达式必须具有以下形式之一:

  • 一个主表达式,例如 Swappable<T>std::is_integral<T>::value(std::is_object_v<Args> && ...),或任何括号内的表达式。
  • 由运算符 && 连接的一系列主表达式。
  • 由运算符 || 连接的一系列上述表达式。
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>(); // error, is_purrable<T>() is not a primary expression
 
template<class T>
void h(T) requires (is_purrable<T>()); // OK

[编辑] 约束的偏序

在进行任何进一步分析之前,约束会通过替换每个命名概念和每个requires 表达式的主体进行规范化,直到剩下的是原子约束的合取和析取序列。

如果可以证明 P 蕴含 Q,直到 P 和 Q 中原子约束的同一性,则称约束 P 包含约束 Q。(类型和表达式不进行等价分析:N > 0 不包含 N >= 0)。

具体来说,首先将 P 转换为析取范式,将 Q 转换为合取范式。当且仅当满足以下条件时,P 包含 Q

  • P 的析取范式中的每个析取子句都包含 Q 的合取范式中的每个合取子句,其中:
  • 当且仅当析取子句中有一个原子约束 U 和合取子句中有一个原子约束 V,且 U 包含 V 时,析取子句包含合取子句;
  • 当且仅当原子约束 A 和原子约束 B 使用上述规则是相同的,原子约束 A 包含原子约束 B
  • 一个折叠展开的约束 A 包含另一个折叠展开的约束 B,如果它们具有相同的折叠运算符,A 的约束 C 包含 B 的约束 C,并且两个 C 都包含一个等效的未展开的包。
(C++26 起)

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

如果声明 D1D2 受约束,并且 D1 相关的约束包含 D2 相关的约束(或 D2 未受约束),则称 D1 至少与 D2 一样受约束。如果 D1 至少与 D2 一样受约束,而 D2 不至少与 D1 一样受约束,则 D1 D2 更受约束

如果满足所有以下条件,则非模板函数 F1 比非模板函数 F2 更受偏序约束

  • 它们具有相同的参数类型列表,省略显式对象参数的类型(自 C++23 起)
  • 如果它们是成员函数,则两者都是同一类的直接成员。
  • 如果两者都是非静态成员函数,则它们的对象参数类型相同。
  • F1F2 更受约束。
template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
 
// RevIterator subsumes Decrementable, but not the other way around
 
template<Decrementable T>
void f(T); // #1
 
template<RevIterator T>
void f(T); // #2, more constrained than #1
 
f(0);       // int only satisfies Decrementable, selects #1
f((int*)0); // int* satisfies both constraints, selects #2 as more constrained
 
template<class T>
void g(T); // #3 (unconstrained)
 
template<Decrementable T>
void g(T); // #4
 
g(true); // bool does not satisfy Decrementable, selects #3
g(0);    // int satisfies Decrementable, selects #4 because it is more constrained
 
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); // ambiguous

[编辑] 注意

功能测试宏 标准 特性
__cpp_concepts 201907L (C++20) 约束
202002L (C++20) 条件性平凡的特殊成员函数

[编辑] 关键词

concept, requires, typename

[编辑] 缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 发布时的行为 正确的行为
CWG 2428 C++20 无法将属性应用于概念 允许

[编辑] 另见

Requires 表达式(C++20) 生成类型为 bool 的纯右值表达式,描述约束[编辑]