1.模板自动推导功能。
先看个例子:
在调用TestTemplate函数时, 我们可以在函数后面加上<类型>无歧义地指定调用的版本。
结果如下:
由于模板参数在函数参数中的位置是固定的,编译器其实可以推导出参数的类型, 这样程序员们就可以不指定模板的类型来调用,代码更加简洁清晰通用,不会出现写错模板类型的错误,如下:
模板自动推导是如此的美好,我们要好好地利用它。 在应用过程中, 也引入了一些问题, 有些情况, 编译器发现某些代码满足多个同名函数模板,无法决定特化成哪一个,便会报错; 或者编译器不报错了,但是我们自己都不确定编译器特化成哪一个, 存在巨大的隐患。
因此,十分有必要搞清楚特化的规则。
2.模板函数推导的优先级。
a.不带模板的函数最优先。
例如增加专门针对int类型的普通函数实现:
结果如下:
当参数是int时, 调用了普通函数的版本, 虽然模板也满足,但是普通函数优先。
如果确实需要调用模板的版本,可以通过指定模板参数类型来实现。
如下:
这样就能调用到模板的版本。
b.模板参数个数越少的越优先(不建议参考)。
我在VS2017测试如下:
TestTemplate接受2个类型T1, T2。以上输出就是调用模板实现。
此处没有歧义,谁赞同,谁反对?
这时增加一个模板函数, 对于T1和T2是同类型时,作出处理。
此时,编译器会调用哪个?是否明确?
我在VS2017上测试,发现调用了模板参数少的,结果如下:
在标准上不知道有没有规定这种情况应该实例化哪个函数, 为了避免引起误会, 建议不能依赖它。 写代码时要避开有歧义的坑, 确保读者写者都对含义的理解完全一致。 后面std::enable_if完全可以清晰地表达我们想要编译器实例化成哪个函数。
3.模板类型推导冲突。
当一个模板函数推导,没有特化满足,或者多于一个特化满足时,编译器都会报错,罢工。
假设我要实现一个转换函数,让不同的类型相互转换,例如short转成int, int 转成string等。
其中从std::string转换成基本类型,或者基本类型转换成std::string, 我希望引入第三方的串行化库来实现,这时,分别对这两种情况进行了模板特化。
以上代码运行良好,输出如下:
当我们增加一行从std::string转换到std::string的模板调用时, 编译器就不干了。
因为这个调用, 以上3个模板定义都满足, 而且无法通过指定模板类型的方法来解决。Transform<std::string>指定了模板参数类型后,依然是冲突的。
解决方案一:
增加一个std::string转std::string的普通函数重载,根据普通函数优先的规则, 编译器锁定了调用的版本,不再有歧义。缺点就是代码对于std::string转std::string的逻辑表达上是冗余的,同一份逻辑,在两个地方重复定义了,而重复是万恶之源。 对于在c++11以前, 也没有更好的表达办法了。
解决方案二:
在语法上明确地告知编译器,我们想要它怎样去选择。在c++11以后,我们有了这样的表达工具,那就是std::enable_if。具体用法在后面介绍。
4.std::enable_if用法
enable_if 的主要作用就是:当某个条件成立时,enable_if表示指定的类型;当条件不成立时, enable_if表示未定义,但不会报错,编译器在实例化时就会忽略它,最终定位在唯一符合实例化的实现。声明如下:
template<bool Cond, class T = void> struct enable_if;
a.SFINAE特性
SFINAE 全称是 Substitution Failure Is Not An Error。 当模板定义中出现了一些符合c++语法,但是不存在的函数,或者变量等。只要没有真正实例化, 也不会报错。
例如:
T::testError函数,在没有实例化之前, 编译器都不会去考虑它是否存在,是否合法。只要不违反c++语法就可以了。
违反C++语法的,即使没有实例化,也会报错的。例如输入了一些中文:
b.有了SFINAE特性, std::enable_if才能放心地大展拳脚。
现在我们来解决前面提到的std::string转换成std::string的特化问题。
先写出了Transform函数的通用表达如下:
之前的所有Transform调用都是通过的。
typename std::enable_if<true, "类型">::type 等价于 "类型"
typename std::enable_if<false, "类型">::type 等价于 未定义
<==>表示等价于, 那么:
typename std::enable_if<true, bool>::tpye <==> bool
typename std::enable_if<true, int>::tpye <==> int
typename std::enable_if<true, T1>::tpye <==> T1
typename std::enable_if<false, bool>::tpye <==> 未定义
typename std::enable_if<false, int>::tpye <==> 未定义
typename std::enable_if<false, T1>::tpye <==> 未定义
未定义表示, 该类型标识所在的函数等价于没有定义, 编译器在实例化时会忽略它。
Transform函数修改如下:
由于std::enable_if中的condition固定为true,
typename std::enable_if<true, bool>::tpye <==> bool
整个模板函数,跟修改前其实时一样的,因此编译也通过。
一旦把std::enable_if里的条件改成false, 编译器便报错。
std::is_same用于判断两个类型是否相同。
std::is_same(T1, T2)::value的结果就是bool类型。
如果T1,T2类型相同,std::is_same(T1, T2)::value = true;
如果T1,T2类型不同,std::is_same(T1, T2)::value = false;
std::is_same等函数与std::enable_if组合起来就可以实现根据类型选择不同的模板特化的精准控制。
例如在以上基础上增加std::is_same(T1, T2)判断:
编译器提示T1与T2类型不相同的模板实例化失败。 而T1和T2类型相同情况,就精准地调用了我们对类型相同判断后的特化。
接下来我们分别对
目标是字符串,但源不是。
以及目标不是字符串, 但源是的情况做特化。
结果准确地如我们所愿:
并且当我们把int转成unsigned short时, 它能准确地报错,告知我们该转换需要另外实现。
std::enable_if用法规则如上, 除了可以用在函数返回值上, 还能
a.用在函数末尾增加一个冗余的参数。
例如:
用在函数末尾,表示函数的最后一个参数的类型根据T1, T2的判断来决定, 判断为true, 则正确定义了一个冗余的类型指针,成功模板实例化, 并且由于指定了默认参数,调用者与原来的调用一致。 类型判断结果为false时, 类型未定义,实例化时自动忽略跳过。 比std::enable_if用在函数返回值更清晰一些。
b.用在模板的参数上。
模板参数支持bool, int, 指针等类型,把std::enable_if用在模板的参数上, 函数的形式与原来完全一致, 我更加偏向于使用这种方式。
最后提醒一点, 前面所有例子对于类型的判别都没有做去除修饰词操作。
例如模板中的T1, T2, 有时候并非推导成某个类型type, 可能是type&, 可能是const type, 也可能是const type&, 为了准确地使用std::is_same等函数判断类型, 通常还需要对T1, T2做一个移除修饰词的操作。 std::decay就是专门做这个事情。
无论T1推导成int, 或者int&,或者const int, 或者const int&, std::decay<T1>::type总是表示int。
因此最终实际使用时, 所有模板标签判断语句都应该加上std::decay。
最终变成:
std::is_same<typename std::decay<T1>::type, typename std::decay<T2>::type>::value,
或者std::is_same<std::decay_t<T1>, std::decay_t<T2>>::value。
c++11 type_traits 增加了很多对于类型判断的功能,例如std::is_integral std::is_class, std::is_enum等...