先说在前头,模板的初衷是泛型,但它不是固定格式,每一个关键字以及符号都有其规则。
这应该算是模板入门的经典模板头部了。 不接触模板元编程的 C++ 用户也很容易将它当作固定格式(像极了初学者觉得 cpp 文件开头一定要书写 using namespace std;),以至于在创建复杂模板或者多个模板配合使用时像无头苍蝇般写到哪儿算到哪儿,于是出现各种不明原因的错误。
这个系列目的在于说明这些规则,让 C++ 用户在写模板的时候知道自己在干嘛。
1、从尖括号说起
先说结论。尖括号里的内容是模板实例化的条件。它要求模板参数必须是类型(typename 的作用,后面会提到)。先看这张图。
千万别把它跟特化那一套规则混淆。这就是单纯的两个模板。函数名可以当作模板标识符, foo<int> 由于尖括号里的内容是类型,符合①的规则,因此这里必然将①实例化。而 foo<1> 尖括号里的内容是个整型,自然就是实例化②了。
也就是说,尖括号在这里是个条件约束符,用于规定满足某种条件时实例化该模板并调用。所以这段代码的报错就很好理解了:
尖括号里要求传入两个类型,而 foo<int> 只传入一个类型,不符合条件,触发 SFINAE 规则,于是继续匹配,直到找不到匹配模板而报错(这里了解 SFINAE 规则是什么)。
2、template 是什么
这个话题在这里还没法细讲。这里只能说,它有两种语境。当它后面跟随的符号为尖括号时,目的在于告诉编译期以下代码块无需实例化,等匹配。其二就是显示模板实例化的内容了,之后在模板特化的部分再细说。
在模板头部的 template 有点 if 的性质,尖括号中的内容如果不匹配,那么找其他同名模板进行匹配(就像 else if )。总之,到目前为止,template 关键字就是告诉模板"没调用到就别实例化"这个信息。
3、typename 是什么
这玩意目的也只是告诉编译器,接下来的内容是个类型。类比一下:
括号里面的 int 或者 char 无非在告诉编译器,它接收的是什么类型的参数。类型匹配则调用对应的函数(函数重载)。所以如果传入 foo 的参数无法隐式转换到 int 或者 char,比如 ostream 类型的参数,那么编译器会报错为找不到重载函数:
也就是说,当尖括号里的条件是 typename 时,必须传入类型才能满足条件,否则找不到匹配的模板(参照开篇第 3 张图)。
其次,这里的 typename 还有第二个作用。这也是之所以模板编程又叫泛型编程的原因。
T 在这里并不是实际存在的类型,而是一个类型形参。typename 的目的在于告诉编译器,这是个类型,不要报错。同样用作这个目的的 typename 可以参考这张图:
上面这张图一共出现两个 typename ( val_1 这行肯定报错,因为单独的 T::type 是个待决名。待决名的规则这里也先挖个坑 ),排除尖括号里的 typename 用作限制类型的目的之外,这两个 typename 作用完全一样,就是为了告诉编译器,这是个类型,不要报错。当然更多的细节要到偏特化才能说清楚。
以上算是先给模板开个头,后续将继续深入。