上文还是三天前写的,这两天连着两个晚上都在追踪一个Bug。程序运行、退出都正常,但是打开特定编译开关后,提示Warning: Thread Sanitizer: data race。程序员是不会容忍代码中有任何隐患存在的。说句题外话,写代码都会遇到Bug(Linux的发明者说过,没有人可以一下子就写出没有bug的代码,除了他自己)。估计没有多少人会喜欢Bug。其实我认为排除Bug是一件非常有趣的事,而且可以学到不少的东西,通常Bug都是来自于我们之前某个方面的错误认知,“唉,原来它是这样的,我一直以为是那样的”,不断地修正我们的认知,才能让我们高效地写出优秀的代码。
还是先回到我们上次的那个函数模板吧
template <typename T>
T max(T a, T b) {
return b < a ? a : b;
}
上文说到在使用这个函数模板时通常不需要显式告诉编译器类型参数T的实际类型,编译器会从调用时实际传入的参数推断出T的类型(也就是从调用参数推断出模板参数),正常情况下编译器都不会打扰你,默默地帮你搞定了。
max(3, 5.6);
不过这次编译器不干了,第一个参数它推断T为int,第二个参数它推断T为double,它不知道T该选哪个,只能向你报告了。“编译器真笨,它就不会把int转换为double吗?”。还真的不会,编译器在类型推断时不会进行相应的类型转换,它只是按照推断准则,独立地利用每个调用参数(a和b都是调用参数)推断出对应的模板参数的类型(这里只有一个模板参数T)。如果通过a和通过b推断出来的T相同,推断成功。现在这里T有两个结果类型,你说编译器该怎么办?
1. max(static_cast<double>(3), 5.6);
2. max<double>(3, 5.6);
上面两个解决方法都可以,第一个是将调用参数a的类型强制转换为double,这样推断T1为double,这个同T2的推断结果一致了;第二个干脆不用编译器推断类型了,我直接告诉你用double来替换T。
还有其他的解决方法吗?上面的方式看起来不够简洁啊。当然了,就像函数参数可以有多个,模板参数也可以不止一个,这样各自使用编译器的推断结果,互不干扰,完美。对,改一下
max(3, 5.6);
这回编译没有问题了,T1推断为int,T2推断为double。哎,不对呀,怎么返回的值是5啊,应该是5.6。哦,明白了,返回类型为T1,这里是int。这里的确切返回类型应该取决于调用参数,不能硬性规定啊。再加一个类型参数?
template <typename T1, typename T2, typename T3>
T3 max(T1 a, T2 b) {
return b < a ? a : b;
}
这回应该可以了吗?很遗憾,不行。T3只在返回值类型上用了,没有哪个调用参数使用它,编译器无法从调用参数里把它推断出来,你只能明确地告诉编译器T3是什么类型。很不幸,你把它放在了模板参数列表的最后一个,指定时不能直接给出最后一个类型,还必须先给出T1和T2的类型。虽然你可以换个顺序,或者把T1用于返回类型,其余的用于函数的调用参数,这样你是只需要给出第一个参数就可以了,后面的还是让编译器去推断。但是每次调用都需要你带上模板参数,这个太繁琐了,而且还容易出错,费力又不讨好。难道真的没有好的办法了吗?编译器不应该是什么都知道吗?
C++11来了,它来了,带着auto,带着decltype,还带着trailing return type,这些可以帮到我们。
template <typename T1, typename T2,>
auto max(T1 a, T2 b) -> decltype(b<a?a:b) {
return b < a ? a : b;
}
函数返回类型的地方这回是个auto,在C++11中它只是一个占位符(这里它不具备任何类型信息),真正的返回值类型是->后面的东西,decltype又是个什么东东?它是个编译期运算符,类似于sizeof,在编译的时候计算出某个表达式的类型。这里它会在编译的时候得出a和b中较大的那个值的实际类型,然后填充到auto的位置上。编译完成后,max就是一个标准的函数模样了(这里其实有个小bug,因为在某些特定情况下推断出来的类型可能是个引用类型,返回一个局部变量的引用?这玩意儿相当于一个空悬指针啊。这个问题我们以后再说)。
C++跟咱们一样,也是不断进取的,C++14简化了上面的定义
template <typename T1, typename T2,>
auto max(T1 a, T2 b) {
return b < a ? a : b;
}
这回那个有点怪怪的->不见了,现在的auto已经不再仅仅是点位符了,它是一个真正的类型了,而且意外收获是上面的那个小bug在这里也没有了(当然如果你非要decltype(auto),想在推断时保留住更多的类型原始信息,那个小bug还在。不过这种小bug很容易对付,一个decay就可以搞定)。
最后再说一点(感觉文章是不是有点长了?如果是,请留言啊),C++11提供了type traits,它们都是类型函数(类型函数?其实对比一下就很容易理解了。普通函数是圆括号里面有若干输入参数,返回计算结果,参数和结果都是值。如果我们把尖括号里面的模板参数列表看成是输入参数,类模板的名字看成是函数名,在类模板中利用模板参数在类中定义某个类型,这个类型我们可以看成是计算结果,核心是类型函数其实是类模板的定义,里面涉及到的不再是值,都是类型。返回值也是有的,通常是bool类型,判定类型参数的某些属性。感觉有点乱?很正常,以后找时间咱们展开捅一下,这层纸就破了)。
#include <type_traits>
template <typename T1, typename T2,>
std::common_type_t<T1, T2> max(T1 a, T2 b) {
return b < a ? a : b;
}
common_type是个STL中的类模板,也是一个所谓的“类型函数”,这里它的输入参数是T1和T2,它的返回类型是类中的类型定义type(这里使用了一个using来定义一个别名,方便使用,其实是typename std::common_type<T1, T2>::type),是T1和T2两个类型的一个通用类型(比如说int和long,那就返回long,它包含了两个类型)。这个其实也正是我们的最初目的,自动找到一个T1和T2都兼容的类型。当然“类型函数”其实很容易写,你也可以自己写一个,按照你的实际要求来返回某个类型。