|
在 C++ 中使用模板时,常常因为向模板类型传了一个不正确的参数导致编译错误,模板的编译报错通常很长并且难以发现具体的错误在哪里,C++20 中引入的 concepts 就是为了解决这一问题。
考虑一个简单的模板函数:
template <typename T, typename U>
void func(T a, U b)
{
// ...
}
这个函数含有两个模板类型参数 T 和 U。在不加任何限制的情况下,T 和 U 可以是任意的内置类型、标准库或我们自定义的类,当编译器遇到 func 函数调用时,只是简单地将模板特化并且将 func 函数展开。
一般情况下,我们使用函数 func 时只会涉及到某一些特定类型的 T 和 U ,(比如 T 一定是整型,U一定是浮点型),这个信息编译器是不知道的。可以通过 concepts 将这个信息告诉编译器:程序中所有出现的 func用到的类型 T 和 U 都满足某些限制条件,如果程序员无意中传递了一个不符合条件的模板参数,那么编译器就可以具体指出来哪个条件没有被满足。
一个 concept 就是这样一些限制条件的集合,限定了模板类型的范围。concept 可以独立地对 T 和 U 进行限制,也可能同时涉及到两者,比如要求 sizeof(T) > sizeof(U)。
先来看一个最简单的例子,定义两个 concept Small 和 Large:

Small 给类型加了限制,要求 T 类型的大小不超过 int 的大小。Large 要求 T 类型的大小超过 int,并且 T 是一个整型。
在模板中使用 concepts 时有几种不同的语法:

第一种写法最简洁,应用情形也最简单,当模板类型参数能很好地被某种已定义好的 concept 描述时,推荐使用这种方式。add1 和 add2 是完全等价的,限定了模板参数 T 需要符合 Small 给定的限制, U 需要符合 Large 的限制。
第二种和第三种写法中加入了 requires clauses,使用 requires 关键字,后面接上一个逻辑表达式。对于具体的模板类型 T,U 和 V,只有当这个逻辑表达式值为真时模板才能正确特化。注意,Small<T> 是一个 bool 类型,所以可以进行逻辑运算。Small<int> 得到 true, 而Small<long long> 得到 false。
requires clause 放在函数名那一行的前面或者后面均可。
如何描述复杂的 concept?
常常会用到比较复杂的 concept,对应于模板类型参数满足一些复杂的条件。复杂的 concept 通常从简单的 concept 和 requires expression 构造出来。
下面定义了一个 random access iterator 的 concept,它首先需要满足 bidirectional iterator 的限制,其次还需要满足形如 i+n、i[n] 等等的表达式是能够被正确编译的,即这些表达式在语法上是正确的。这就等价于要求 Iter 类型已经实现了这些运算符重载。

也可以再加一些限制条件,限定这些表达式算出来的值是什么类型。

所以,当编译器遇到一个用 RandomAccessIterator 修饰的模板类型时,会先核对这个类型是否满足 bidirectional_iterator 的条件,是否实现了上述运算符重载,运算符重载的返回类型是否与 RandomAccessIterator 中给定的一致。只要有一条要求不被满足,编译器就会报错并指出具体是哪一个条件没有被满足,这样就能大大增加模板报错的可读性。
注意:在进行类型限制时,-> 后跟的是一个 type constraint,不是 type。 type constraint 特殊的一点在于它在 < > 中给定的模板参数的个数比正常的少一个。 这是因为,编译器会把 -> 左边的表达式的类型自动插入到 < > 中成为第一个模板参数。
std::same_as 是包含两个模板类型参数的 concept, 对编译器来说 {i - n} -> std::same_as<Iter> 等同于 std::same_as<decltype(i - n), Iter> ,即要求 i - n 的类型与 Iter 类型一致。
如果某个 concept 只含有一个模板参数,那么当它出现在 -> 右边时参数列表就是空的 <>,可以省略不写,下面用刚才定义的 RandomAccessIterator 作为例子:

使用标准库中的 concepts
C++20 标准库中提供了上百种常用的 concept,放在 <concepts> 和 <iterator> 等头文件中。自己定义复杂的 concept 常常伴随着错误和疏漏,所以只要能用标准库 concept 时就应当尽可能多地使用。
比较常用的一些:
std::same_as<T, U>, std::derived_from<Derived, Base>, std::convertible_to<From, To>
std::integral<T>, std::floating_point<T> |
|