int a = add ( 114 , 514 ); // 自动推导
double b = add < double > ( 19.19 , 810 ); // 显式指定
Vector < unsigned > v { 2 , 3 };
在 C++20 之前,我们所能做到的就只有这样了。但是,这种做法显然是存在很大的弊端的:对于模板提供者而言,我们无法限制使用者传入什么类型,这很可能导致使用者使用了预料之外的类型从而导致了未知的结果;而对于模板函数或模板类的调用者而言,没有一个简单的方法可以知道自己的类需要实现哪些接口才能满足模板类型的需求。在上述的例子中,我们可以简单地猜测 add
函数只需要我们实现 operator + ( T )
的重载即可,但是在实际开发中,尤其是模板类,其内部复杂且通常都会嵌套许多其他的模板类,你很难通过看代码来整理出自己需要实现的接口列表。
由于上述提到的问题,C++ 为其标准库制定了一套具名要求(Named Requirement) ,标准库中某些模板参数会要求你传入的类型满足某一种具体的具名要求,例如,标准库中有一个函数用来随机打乱容器中的元素,其声明为:
在该函数的解释 中,模板参数 RandomIt
被要求 ValueSwappable
以及 LegacyRandomAccessIterator
,通过进一步查阅二者的定义,我们就能知道我们需要为自己的类实现什么接口才能将其作为该模板函数的模板参数使用了。
具名要求某种程度上已经提供了一种新语法的雏形,我们不禁会想:如果能把这种限制直接写进代码中,而不是通过文档的形式体现该有多好!
是的,在 C++20,我们所想的已经成为了现实。
让我们回到 add
函数之中,在 C++20,我们可以这样定义它:
concept Addable = requires ( T x , T y ) { x + y ; };
template < typename T > requires Addable < T >
T add ( T a , T b );
相比于之前只使用模板的写法,这种写法显然复杂了许多。
概念的定义
首先,在定义函数之前,我们先定义了一个概念(Concept) :事实上,“概念”这一概念几乎可以和上文所说的“具名要求”画上等号,它就可以视为以代码形式写出来的具名要求。
概念的定义以 concept
关键字 开头,之后紧跟该概念的标识符,在此处我们使用 Addable
,接着跟随 =
为其赋值。
为一个概念赋值的方法有很多,其中一种办法就是使用 requires
关键字 引入一系列要求的集合,这些要求可以是:
表达式是一个不求值的操作数;只检查语言的正确性,例如上文中的:
只要 T
能够令 x + y
通过编译,即 T
类型有重载过 operator + ( T )
,T
就满足概念 Addable
。
这种要求也可以使用函数调用的形式,用来判断类型 T
是否定义了指定名字的成员函数。同样,该函数只检查正确性,不会真的被调用。
类型要求是关键字 typename 后面接一个可选限定的类型名称。该要求是指命名的类型是有效的:
// 并且 类型 T::inner 必须存在
// 并且 *x 的结果必须可以转换为 T::inner
{ * x } -> std :: convertible_to < typename T :: inner > ;
其中,std :: convertible_to < From , To >
是标准库 concepts
中定义的概念之一,要求类型 From
可以转换为类型 To
。在本例中,第一个参数 From
将自动填入 * x
的返回值类型。
可以在要求中指定其他要求,例如:
此例中,我们使用 Addable < T > && std :: integral < T >
作为新概念 IntegralAddable
的值。Addable
和 std :: integral
是已有的两个概念,我们使用 &&
符号将它们连接,意味着要想满足概念 IntegralAddable
,必须既满足 Addable
又满足 std :: integral
。
作为特例,有一种很特殊的概念:
template < typename T >
requires requires ( T a , T b ) { a + b ; } // 注意 requires 需要使用两次;
T add ( T a , T b )
template < typename T >
T add ( T a , T b )
requires requires ( T a , T b ) { a + b ; } // 注意 requires 需要使用两次;
在你的要求集合比较短,并且只使用一次不想专门为其定义一个概念时,可以使用这种方法。
最后还有一种方法是引入 auto
关键字:
// a 的类型由 auto 推导后作为 From 模板类型传递给 std::convertible_to<From, To>
// 而显式指定的 int 类型则作为 To 模板类型传递给 std::convertible_to<From, To>
// 对于拥有更多模板类型参数的概念也是同理,auto 推导的类型永远是第一个模板类型
int toInt ( std :: convertible_to < int > auto a ) {
return a ;
在对多个泛型参数进行约束时,我们也同样可以使用 &&
, ||
以及 !
,例如:
template < typename T , typename U >
requires Addable < T > && std :: equality_comparable_with < T , U >
bool add_then_eq ( T a , T b , U c )
return a + b == c ;
该模板函数要求 T
类型可相加,并且 T
类型和 U
类型可以比较相等性。
lambda 与概念
在 C++14 中,我们迎来了泛型 lambda,为了不引入复杂的模板语法,C++14 为我们提供了简单易懂的 auto
语法:
// 这是一个泛型 lambda,可以传入任意类型的 a、b 参数,甚至 a 和 b 可以不是同一个类型
auto add = []( auto a , auto b ) {
return a + b ;
然而,在 C++20 中,为了能够让泛型 lambda 也享受到概念语法带来的福利,最终 C++ 委员会还是选择将模板列表带给了 lambda(这何尝不是一种)。
当然,我们仍然可以使用下面这种方法抗拒模板列表:
同样的,requires
子句也可以放在形参列表的后面,也可以将 Addable
替换掉 typename
,或者直接将概念 Addable
的定义替换上来。
标准库 concepts
在 C++20 中,标准库新增了头文件 concepts
,其中定义了许多常用的概念可以供我们直接使用,此处介绍一些常用的标准库概念:
same_as<T, U>
当 T
和 U
为同一类型时才满足
derived_from<Derived, Base>
当 Base
为 Derived
或是 Derived
的基类时才满足
convertible_to<From, To>
当 From
类型能够隐式和显式转换为 To
类型时才满足
integral<T>
当 T
类型为整数类型时才满足
signed_integral<T>
当 T
类型为有符号整数类型时才满足
unsigned_integral<T>
当 T
类型为无符号整数类型时才满足
floating_point<T>
当 T
类型为浮点类型时才满足
assignable_from<LHS, RHS>
当 RHS
类型的表达式能够赋值给 LHS
左值时才满足
swappable<T>
当 T
类型的左值可交换时才满足
swappable_with<T, U>
当 T
类型的左值和 U
类型的左值可互相交换时才满足
constructible_from<T, Args...>
当 T
类型可以由参数类型集 Args...
构造时才满足
default_initializable<T>
当 T
类型能够默认构造时才满足
move_initializable<T>
当 T
类型能够移动构造时才满足
copy_initializable<T>
当 T
类型能够拷贝构造和移动构造时才满足
equality_comparable<T>
当运算符 ==
与 !=
能反应 T
类型上的相等性时才满足
equality_comparable_with<T, U>
当运算符 ==
与 !=
能反应 T
类型与 U
类型之间的相等性时才满足
totally_ordered<T>
当比较运算符在 T
类型上严格全序时才满足
movable<T>
当 T
类型可移动,即:能移动构造、移动赋值,左值能交换时才满足
copyable<T>
当 T
类型可拷贝,即:既可以拷贝构造,又可移动时才满足
semiregular<T>
当 T
类型既可拷贝,又可以默认构造时才满足
regular<T>
当 T
类型既可拷贝,又可以默认构造,并且可以比较相等性时才满足
需要注意的是,与类型相关的概念,仅仅只是检测其类型是否为标准类型,而非检查其语义。这意味着 int
和 const int
满足 integral < T >
,但是 int &
和 const int &
却不满足 integral < T >
,例如:
template < typename T >
concept IntegralArrayType = requires ( T a ) {
{ a [ 0 ] } -> std :: integral ;
a . size ();
void do_nothing ( IntegralArrayType auto a ) {}
int main () {
std :: vector < int > vec ;
do_nothing ( vec );
这样的写法是无法通过编译的,因为 a [ 0 ]
在此处返回的类型是 int &
,其不满足 std :: integral
的约束。
想要让这段代码工作,我们可以引入一个新的概念来表示整数类型或者整数类型的引用:
template < typename T >
concept Number = std :: integral < typename std :: remove_reference < T >:: type > ;
template < typename T >
concept IntegralArrayType = requires ( T a ) {
{ a [ 0 ] } -> Number ;
a . size ();
void do_nothing ( IntegralArrayType auto a ) {}
int main () {
std :: vector < int > vec ;
do_nothing ( vec );
2023/04/24 初探 C++20 Coroutine
前言 近段时间研究了一下 C++20 的协程(Coroutine),大概了解了其中的工作原理,做一下记录。 初次接触 Coroutine 时,给我的感觉是一脸懵逼的。和其他语言简单的 async、await 不同,想要使用 C++20 的 Coroutine,它要求你定义一个包含 promise_type 的类型,其中 promise_type 又需要至少包含 get_return_ob...
2023/07/18 [C++] 不使用标准库和 lambda 实现柯里化
限制 由于 C++ 提供了 lambda 语法和强大的 functional 库,如果不做任何限制的话,那么实现柯里化是一件非常简单的事情。本文注重于介绍原理和过程,而不是最终结果,因此,让我们来做一些大胆的限制: 禁止使用任何标准库组件 禁止使用 lambda 语法 实现 Function 事实上,在我之前的文章 [C++] std::function 是如何实现 la...
2023/06/28 [C++] 深入了解左值与右值
C:左值与右值 最初,C 语言中的左值(lvalue)意味着任何可以赋值的东西,因为它们可以放在赋值等号的左边,因此它们被命名为左值;相反地,那些只能放在赋值等号右边的东西就被称为右值(rvalue)。 时过境迁,随着 C 语言的版本迭代,这种分类方法已经不再具有价值,左值和右值的定义也随之发生改变。 但是在开始之前,我们需要特别明确一个概念:左值和右值在 C/C++ 中是表达式(ex...
Qt 计时器 QTimer 的妙用
Android C++ 生成 compile_commands.json
© 2024 Nichts Hsu . 保留部分权利。
本站采用 Jekyll 主题 Chirpy