一文读懂C++20新特性之概念、约束(concept, constraint)
约束与概念是C++20中最新引入的核心语言特性。约束(constraint)可以关联到类模板、函数模板、类模板成员函数,指定了对模板实参的一些要求,这些要求可以被用于选择最恰当的函数重载和模板特化。概念(concept) 是这些要求(即约束)的集合。
概念(concept)
语法:
template < 模板形参列表 >
concept 概念名 = 约束表达式;
例子:
template<class T, class U>
concept isChildOf = std::is_base_of<U, T>::value;//类型约束, T必须继承自U
/***
使用概念
注意:概念在类型约束中接受的实参要比它的形参列表要求的要少一个,
因为按语境推导出的类型会隐式地作第一个实参
***/
template<isChildOf<Base> T>
void f(T); // T 被 isChildOf<T, Base> 约束
组成概念的约束表达式也可以用requires字句定义:
//以下代码摘自cppreference: https://en.cppreference.com/w/cpp/language/constraints
#include <concepts>
// 概念 "Hashable" 的声明可以被符合以下条件的任意类型 T 满足:
// 对于 T 类型的值 a,表达式 std::hash<T>{}(a) 可以编译并且它的结果可以转换到 std::size_t
template<typename T>
concept Hashable = requires(T a)
{
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
//在函数模板中使用概念
template<Hashable T>
void f(T); // 受约束的 C++20 函数模板
也可以按如下格式使用概念:
template<typename T> requires Hashable<T> //requires子句放在template<>之后
void f(T)
{
//...
}
template<typename T>
void f(T) requires Hashable<T> //requires子句放在函数参数列表之后
{
//...
}
注意:
概念本身不能被约束,概念不能被递归定义。
//以下代码摘自cppreference: https://en.cppreference.com/w/cpp/language/constraints
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 子句试图约束概念
requires关键字
1. requires关键字可以用来引入require子句
在这种情况下,requires后面必须跟随一个常量表达式,或者满足如下形式的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>(); // 错误:is_purrable<T>() 不是初等表达式
template<class T>
void h(T) requires (is_purrable<T>()); // OK
2. requires关键字也用来开始一个 requires 表达式
此时,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; }
require表达式具有如下语法:
requires { 要求序列 }
requires ( 形参列表(可选) ) { 要求序列 }
其中,要求序列根据复杂程度可以分为以下四种:
- 简单要求(simple requirement)
- 类型要求(type requirement)
- 复合要求(compound requirement)
- 嵌套要求(nested requirement)
// 简单要求
template<typename T>
concept Addable = requires (T a, T b)
{
a + b; // “表达式 a + b 是可编译的合法表达式”
};
// 类型要求
template<typename T>
using Ref = T&;
template<typename T>
concept C = requires
{
typename T::inner; // 要求的嵌套成员名
typename S<T>; // 要求的类模板特化
typename Ref<T>; // 要求的别名模板替换
};
// 嵌套要求
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] }; // 复合
};
复合要求有自己的语法,因此我们将它单列出来,其语法形式为:
{ 表达式 } noexcept(可选) 返回类型要求(可选) ;
返回类型要求 -> 类型约束
例子:
// 复合要求
template<typename T>
concept C2 = requires(T x)
{
// 表达式 *x 必须合法
// 并且类型 T::inner 必须合法
// 并且 *x 的结果必须可以转换为 T::inner
{*x} -> std::convertible_to<typename T::inner>;
// 表达式 x + 1 必须合法
// 并且 std::Same<decltype((x + 1)), int> 必须被满足
// 也就是说,(x + 1) 必须是 int 类型的纯右值
{x + 1} -> std::same_as<int>;
// 表达式 x * 1 必须合法
// 并且它的结果必须可以转换到 T
{x * 1} -> std::convertible_to<T>;
};
约束的影响
当编译器在进行模板函数的重载决议时,会选择更受约束的版本
template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
// RevIterator 能归入 Decrementable,但反之不行
template<Decrementable T>
void f(T); // #1
template<RevIterator T>
void f(T); // #2,比 #1 更受约束
f(0); // int 只满足 Decrementable,选择 #1
f((int*)0); // int* 满足两个约束,选择 #2,因为它更受约束
本文只是列举了“概念”,“约束”等新特性的表面用法,笔者自身水平还不能做更深入讨论,如有遗漏,欢迎广大网友补充,批评指正。
本文参考自:Constraints and concepts (since C++20) - cppreference.com