在 C++98/03
的泛型编程中,模板实例化有一个很繁琐的地方,就是连续两个右尖括号 >>
会被编译解释成右移操作符,而不是模板参数表的形式,需要一个空格进行分割,以避免发生编译时的错误。
template<size_t i>
class X {};
template<class T>
class Y {};
int main()
{
Y<X<1>> x1; // 在c++11中编译通过
Y<X<2>> x2; // 在c++98中编译失败
return 0;
}
在实例化模板时会出现连续两个右尖括号,同样 static_cast
、 dynamic_cast
、 reinterpret_cast
、const_cast
表达式转换时也会遇到相同的情况。 C++98
标准是让程序员在 >>
之间填上一个空格,而在 C++11
中,这种限制被取消了!
在 C++11
标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出 >>
是一个右移操作符还是模板参数表的结束标记。
模板别名其实就是就是利用模板和起别名两个特性结合起来!
比如有以下场景,我们有一个 map
,map
里面放的第一个参数固定是 string
类型,但是第二个参数是未知的,而如果我们有好几个实例化对象都是不同类型的,那么在 c++98/03
的时候我们想给每个这种类型起别名的话,只能一个一个 typedef
,如下所示:
typedef map<string, int> mint;
typedef map<string, float> mfloat;
// ......
那么这样子就非常复杂,考虑到有大量重复的都是 string
,而变化的只有第二个参数,我们可以利用模板:
template <typename T>
typedef map<string, T> mt;
mt<int> mint;
mt<string> mstr;
// ......
但是疑惑的是上述的定义不能通过编译!因为 c++98/03
并不支持这种语法,但是我们可以通过类来包装一下,照样能达到效果,如下所示:
template <typename T>
struct alias_mt
{
typedef map<string, T> map;
}
alias_mt<int> mint;
alias_mt<string> mstr;
// ......
通过包裹类的方法虽然可以实现上述的需求,但是一看代码就觉得,这个代码可读性差,不就是定义一个变量吗,还需要整一个包裹类来封装下?
幸运的是 C++11
终于让我们可以不用通过上述这种臃肿的方式来实现这个需求了。在 C++11
中,新增了一个特性就是可以通过使用 using
来为一个模板定义别名,比如说上述的需求,使用 C++11
就可以这样实现:
template <typename T>
using alias_mt = map<string, T>;
alias_mt<int> mint;
alias_mt<string> mstr;
实际上 using
包含了 typedef
的所有功能,来看下使用 using
关键字和 typedef
关键字定义普通类型别名的用法。
#include<iostream>
#include<type_traits>
typedef int int32; // 通过typedef 给一个类型起别名,不是新建类型
using my_int = int; // c++11的方式
int main()
{
// is_same是判断两个类型是否一致,如果是则返回真,否则返回假
std::cout << std::is_same<int32, my_int>::value << std::endl;
return 0;
}
// 运行结果
1
可以看到在对普通类型的别名定义上,两种方法的使用基本等效,唯一不同的仅仅是定义的语法,using
使用起来就像是赋值,但是在定义函数函数指针的时候,using
的可读性要好一点,比如下面的语句:
typedef void(*func)(int, int);
using func = void(*)(int, int);
可能突然看起来使用 using
的方式来定义一个函数指针有点怪,但是习惯了之后会发现使用 using
这种赋值的方式更适用开发人员的思考方式。下面再显示一个通过 typedef
和 using
方式分别来定义一个函数模板的例子:
template <typename T>
struct FuncSt
{
typedef void(*func)(T, T);
};
FuncSt<int>::func func_typedef;
-----------------------------------------------------------
template <typename T>
using func_using = void(*)(T, T);
func_using<int> func;
可以看到通过 using
定义模板别名的语法,仅仅是在普通类型别名语法基础上增加了 template
参数列表,通过 using
可以轻松的创建一个模板的别名,而不需要像 C++98/03
那样增加一个包裹类。
但是需要额外注意的是使用 using
或者 typedef
仅仅是定义一个别名,不会创造新类型。
在 C++98/03
里,类模板是支持默认的模板参数的,比如:
template <class T, class U = int, U v = 0>
struct test {
// ......
};
但是在 C++98/03
中却 不支持函数模板的默认模板参数:
template <class T = int> // ❌不支持
void func(T x) {
// ......
}
现在这个限制在 C++11
中已经解除,上述的定义在 C++11
中可以直接使用了。
在函数模板中当所有模板参数都有默认参数时,函数的调用就如同普通的函数调用,但是对于类模板而言,哪怕所有模板参数都有默认构造函数在使用时还是必须在模板名后跟随 <>
来实例化。
C++11
中函数的默认模板参数在使用规则上和其他的默认参数也有一些区别,普通函数的默认参数必须写在参数列表的最后,而函数的模板参数就没有这个限制,因此当使用默认模板参数和模板参数自动推导时就显示十分灵活,可以指定函数中的一部分参数是默认参数,另一部分采用自动推导。比如:
template <typename T = int, typename U>
T func(U val)
{
//...
}
int main()
{
func(123);
return 0;
}
但是如果在使用函数模板时如果显示指定模板的参数,由于模板参数的填充顺序是自左向右的,因此像下面这样的调用返回的类型是 long
类型:
func<long>(123); // func返回类型是填充类型long
这个细节虽然简单,但是在多个默认模板参数和多个模板参数自动推导穿插使用时会容易被忽略掉,造成使用上的一些意外,建议在使用的时候尽量还将默认模板参数写在模板参数的末尾。
另外当默认模板参数和自动参数推导同时使用时,若函数模板无法推导出参数类型时,编译器将使用默认模板参数,否则将使用自动推导的参数类型。这个跟函数的默认参数使用规则是一致的,比较好理解。
模板别名以及默认模板参数是在泛型编程中的一些小细节,是 C++11
对 C++98/03
一些细节上的提升,因此介绍的篇幅不多,主要是在使用的时候若可以的话可以通过这些小技巧增加代码可读性,减少代码冗余。