解剖C++模板(2) —— 模板匹配规则及特化
众所周知,模板声明部分的尖括号中的内容是声明模板形参,而调用模板时的尖括号是给模板传参。然而这样理解仅仅停留于现象,只是将模板形参传参和函数传参的过程划等号了。C++ 的函数重载匹配并非真的进行匹配,因为函数名修饰规则导致重载总能第一时间找到匹配的函数。而模板不同,对模板传参进行实例化过程中,模板匹配的过程是真实发生的。
先谈实例化条件。实例化条件不同,即使模板标识符相同,也会被当作不同模板。对应的就是实例化模板时的匹配规则。这里需要分清实例化条件和匹配规则。匹配规则是根据不同模板所要求的实例化条件,对模板实例化语句进行条件查找,以找到最合适的模板进行实例化。
对于不同类别的模板,对多个同名模板实例化条件的要求也各不相同,实际上对应的就是匹配规则的不同。我们先谈实例化条件的要求,也就是同名模板中尖括号的规则。
1、函数模板
1、函数模板规则
函数模板的实例化条件是最宽松的,同样数量的模板形参,但类别不同,依然当作不同模板处理。模板形参个数不同也会被当作不同模板(其中讲类当作模板参数必须 C++20 以上才支持)。这一切都是为了配合函数重载。函数模板支持以类型推导来隐式实例化,也正因为如此,函数模板的规则是最麻烦的一种,它的理解有别于其他模板,甚至可以说与其他模板的实例化规则不是同一套(具体等谈完其他类别模板之后再细说)。
因为函数模板对实例化条件的要求过于宽松,加上模板由于有默认模板形参的存在,类比于函数重载会引发重载歧义,函数模板也会引发模板实例化歧义。
2、为什么函数模板没有偏特化
基于以上所提及的特性,模板没有偏特化的第一个原因:对于指定其中某个模板形参的这种偏特化,函数模板压根不需要这种形式。原因如下图:
这不就是模板偏特化么?只是不支持像类模板那种偏特化写法罢了。
至于第二种偏特化,即对传入类型加以限定的偏特化,许多情况下都会导致编译器无法判断以原始模板还是偏特化进行实例化。先看个例子,虽然这个例子并不贴切。
也就是说,当你不显式指明函数模板形参时,编译器根本无法判断你是想传值还是传引用。假如此时你对模板形参以引用进行限定偏特化,编译器如何判断你传入的右值函数参数是要传值还是传引用?应该选择哪个版本进行实例化?毕竟函数传引用和传值都是特别常见的操作,根本不存在优先级。除了引用问题,const 偏特化也是一样的情况,可自行类比于下图的函数重载歧义。
总结来说函数模板之所以没有偏特化一部分原因是不需要,另一部分原因是做不到。归根结底还是由于函数模板对类型推导的支持所导致。
2、其他模板
这部分我们以类模板作为典型,其他模板也是一样的规则。
1、类模板规则
将刚才的函数模板直接改为类模板会发现编译根本通不过。
这源于类模板是显示写明模板参数的。在模板实例化开始时,会将所有模板添加到模板匹配列表里。类模板与函数模板不同的是,同名模板在匹配列表中只允许存在一个,并且写在前面的模板会排斥后续的同名模板。以下三张截图就足以清洗说明这一点。
① 和 ② 说明两个同名模板的模板形参个数不同,编译器将写在前面的同名模板添加到匹配队列,导致后面的模板压根进入不了匹配队列,因此编译器只认识前面的模板,故而会提示模板形参太多或者缺少模板形参。 ③ 之中,两个同名模板的模板形参个数也相同,但形参类型不同。但不管是哪种情况,编译器只会将第一个模板添加到匹配队列中。
2、类模板的全特化和偏特化
毕竟类模板实例化必须要显式传递模板参数,它的匹配规则并不需要像函数模板一般复杂。不论是全特化还是偏特化,只要显式写明模板参数就不会造成歧义。如同函数模板中 foo<int&&>(std::move(n)) 的例子一样。
前面提到的类模板的同名模板在匹配列表中只允许存在一个。而全特化偏特化则是添加一个限制后的与主模板同名的模板到匹配列表中。事实上我们完全可以如同函数模板一般,将偏特化当作一个新的同名模板
3、变量模板和约束
这里仅作展示,匹配规则与类模板是一样的。
可见约束和类模板一样,只允许有一个同名约束。
对于变量模板,VS 甚至会在书写模板时就对第二个同名模板报错。
4、总结
以上结论,模板应该区分为函数模板和非函数模板,他们的匹配规则是不同的。而匹配规则便是尖括号中的内容,它对模板实例化起决定作用。
文中还提到匹配列表,它是如何运作的,后续有机会再谈。