C++20前模板需要给类型添加限制时,比如需要给一个函数模板的参数类型限制为整形,可以使用c++11中的std::enable_if
和std::is_integral
:
1 | template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>> |
即通过额外的类型匹配来完成类型约束,你可以在stl源码中找到大量的enable_if
约束。
但这种做法有几个问题:
- 复用麻烦,当多个模板需要相同的类型约束时,只能将相同代码复制多份
- 约束失败时错误不明了,如上模板传入浮点类型时,编译器只会抱怨
enable_if<false, void>
内没有type
类型,如果约束条件更复杂,得到的编译错误将更难以阅读
综上原因,20标准往前走了一步,将类型约束(Constraint)进行了规范,并提出了新的“概念”(Concept)。
约束 Constraint
标准里将约束定义为对模板参数的逻辑操作和操作数序列,20标准里约束可以出现在两种场景,模板requires
子句中及concept
的主体中。
requires关键字
20添加了requires
关键字用以编写约束,requires
关键字可以出现在两种场景:模板requires
子句和requires
表达式
requires子句
以上面的整型约束为例,改写为requires
子句为:
1 | template <typename T> |
requires
后的表达式求值结果必须为编译期常量bool值,可以使用逻辑与&&
(标准上称为conjunctions 合取)、逻辑或||
(标准上称为disjunctions 析取)联立表达式,也就是可以如下编写:
1 | template <typename T> |
逻辑操作短路求值,以上代码中前两项已满足,编译器不再校验int::value
类型的存在,godbolt编译
requires 表达式
标准委员会可能认为requires
子句写出复杂的约束还是不够简洁,于是就有了requires
表达式,以上requires
子句的例子可以改写为:
1 | template <typename T> |
我特意使用了多个requires
,是想说明下这里各个requires
的作用,否则一个requires
子句就可以了。
上例中的
1 | requires { |
就是一个requires
表达式,标准规定其求值结果为bool类型的纯右值,但我感觉形容为编译期bool常量更合适,所以才可以放到requires
子句的右边。
语法为:
1 | requires ( 形参列表 (可选) ) { 要求序列 } |
形参列表即函数的形参列表,一些要求会涉及变量操作故引入,但这些参数仅作标记存在,仅在编译期校验要求使用;形参列表不能出现默认参数,不能出现可变参数。
要求序列内以分号隔开多个要求,多个要求必须同时满足整个表达式才为true
,要求按类型分为:
简单要求 Simple requirements
1 | template <typename T> |
以上a + b
即简单要求,编译期会校验这个表达式的有效性,类型T是否存在可用的operator+
,其a + b
结果并不会被实际求值。
类型要求 Type requirements
语法:
typename 标识符
用于校验这个类型是否有效:
1 | template <typename T> |
复合要求 Compound requirements
语法:
{ 表达式 } noexcept(可选) -> 类型约束(可选);
如:
1 |
|
以上约束的含义为t + 1
表达式必须有效,且相关操作必须声明为noexcept
,且整个表达式返回值类型必须为T
。std::same_as
为20标准新增<concept>
头中的“概念”(有关见本文“概念”章节),表示类型必须一致(包含cv ref性质)。
虽然标准中未要求Compound requirements的类型约束必须为concept
,但我实际测试下来,目前三大主流编译期此处只能使用concept
进行约束。
嵌套要求 Nested requirements
语法:
requires 约束表达式
也就是requires-clause的语法也可以出现在这里:
1 | template <typename T> |
当然上面只是为了演示,需要注意的是嵌套要求内不能再出现requires表达式嵌套。
requires 示例
1 | // requires子句内包含requires表达式 |
约束规范化 Constraint normalization
约束表达式转化为替换类型之后的多个原子表达式的合取于析取序列的过程,称为约束规范化。
规范化过程类型参数替换后,如果产生了无效的表达式,则该约束非良构,不要求诊断,不参与编译候选。
约束偏序 Partial ordering by constraints
当有多个模板约束同时满足时,就会产生最佳匹配问题,这也就是约束偏序要解决的问题。
标准规定如果约束P蕴含约束Q(或称约束P归入Q,“a constraint P subsumes a constraint Q”),则约束P相比约束Q更严格,更受偏序约束,重载决议将选择更严格的P版本:
偏序例1
1 | // 约束偏序例1 |
以上第二个约束P(整型且为有符号整型)成立时第一个约束Q(整型)必定成立,则P蕴含Q。约束偏序例1 godbolt。
偏序例2
但蕴含关系不检查实际意义上的蕴含,仅作具名“概念”concept的检查:
1 | // 约束偏序例2 |
标准中虽然没有明确说明约束偏序只针对具名的概念有效,但从目前各编译起器支持情况看,编译器不会检查具体约束条件,即使上例中"A && B"已经明显蕴含"A",编译器最终仍会报错无法进行重载决议:约束偏序例2 godbolt。顺带提一下,上例中换成requires
表达式仍然是同样情况。
偏序例3
1 | // 约束偏序例3 |
同样的情况发生在上例这种概念的定义内,编译器无法认定SignedIntegral
蕴含Integral
。约束偏序例3 godbolt
偏序例4
1 | // 约束偏序例4 |
但只要把上例中的SignedIntegral
中的条件改为使用Integral
概念,即可通过编译:约束偏序例4 godbolt。
实际上std::integral
与std::signed_integral
正是这样定义的,也就是说编译器检查约束偏序必须包含concept
,然后再这基础上校验了逻辑表达式的蕴含关系(具体规则在标准上有,这里不赘述,有兴趣的可以查看参考链接中"Partial ordering by constraints"章节)。
概念 Concept
在介绍完约束之后,再来看concept
就比较简单了,在上文中也出现了concept
的使用。
概念concept
即对其模板参数进行了约束的模板,用以解决之前标准中类型约束无法复用的问题,concept
在20标准中作为新关键字被添加。
Concept 语法
1 | template < 模板形参列表 > |
属性即如[[deprecated]]
的列表,上文介绍的requires
子句及requires
表达式即是常见的约束表达式,但实际上任意编译期bool常量都可作为约束表达式。所以,只要你想,你可以写出:
1 | template<typename T> |
编译结果见godbolt编译
Concept 相关规范
concept
定义时不能出现类型约束,即:
1 | template<typename T> |
这里的requires true
是不被允许的,原因也很显然
concept
定义时不能递归定义:
1 | template<typename T> |
虽然标准中没有明确的说明,但各编译器实现下来是无法如上定义的。
-
concept
不能被显式实例化、显式特化、部分特化。虽然同样是顶着template
的前缀,但诸如Truth<int>
、template<> Truth<int>
的写法显然是没有意义的。 -
概念出现在约束表达式中被求值时,满足即为true,否则为false
-
概念约束可以出现在模板形参定义时,以这种方式定义时,概念接受的类型实参会比形参少一个,即:
1 | template<typename T> |
小结
综上,20标准对类型约束的规范和概念的加入,解决了本文开头提出的复用和编译错误难以定位问题,使c++的类型约束语法更加清晰。