目录
一、函数模板
1.函数模板定义
2.非类型模板参数
二、类模板
1.类模板成员函数
2.类模板名的使用
三、typename的作用
1.作用域运算符的作用
2.typename的作用
四、默认模板参数
1.类模板
2.函数模板
五、普通类的成员函数模板
六、类模板的成员函数模板
七、模板显式实例化与声明
1.类模板显示实例化定义及声明
2.函数模板显示实例化定义及声明
3.其他
八、typedef & using
1.typedef
2.using
(1)using定义模板别名
(2)using定义普通类型
(3)using定义函数指针
3. 总结
九、模板全特化与偏特化
1.全特化
(1)类模板全特化
(2)函数模板全特化
2.偏特化
(1)模板参数范围上的偏特化
模板一般分为函数模板和类模板。模板只有被使用时才会被实例化。
template //T为类型参数
T Add(T a, T b)
{
return a + b;
}
上面这段代码就定义了一个函数模板(也称模板函数),相当于定义了一个公式,或者相当于定义了一个样板。
函数模板的定义并不会导致编译器生成相关代码,只有调用这个函数模板时,编译器才会实例化一个特定版本的函数并生成函数相关代码。
编译器生成代码的时候,需要能够找到函数模板的函数体部分,所以函数模板的定义通常都是在.h头文件中。
template
上述的T,因为前面是用typename来修饰,所以T代表一个类型,是类型参数。
在这个模板参数列表里,还可以定义非类型参数。类型参数表示的是一个类型,而非类型参数表示的是一个值。既然非类型参数表示的是一个值,当然就不能用typename/class来修饰,而是要用以往学过的传统类型名来指定非类型参数,例如非类型参数s是一个整型,那就写成int s。当模板被实例化之后,这种非类型模板参数的值或者由用户提供,或者由编译器推断,都可以。但这些值必须都得是常量表达式,因为实例化这些模板是编译器在编译的时候来实例化的(只有常量表达式才能在编译的时候把值确定下来)。
非类型的模板参数,参数的类型还是有一定限制的:
template
int Add()
{
return a + b;
}
int main()
{
Add<10,20>();
return 0;
}
函数模板中,有时需要提供模板参数,有时编译器自己推断模板参数。
但是类模板有点不一样:编译器不能为类模板推断模板参数。所以,为了使用类模板,必须在模板名后面用尖括号“<>”提供额外信息,这些信息其实就是对应着模板参数列表里的参数。
template
class TEST {
public:
};
template后面的“<>”中如果有多个模板参数的话,参数之间要用逗号分隔,类模板也支持非类型模板参数。
TEST是类模板名,不是一个类型名(或者说是一个残缺的类型名),类模板是用来实例化类型的。所以TEST<int>、TEST<double>或者TEST<string>才是真正的类型名(实例化了的类模板)。所以可以看出,一个通过类模板实例化了的类类型总会用尖括号包含着模板参数。
对于类模板,因为实例化具体类的时候必须有类模板的全部信息,包括类模板中成员函数的函数体具体内容等,所以,类模板的所有信息,不管是声明,还是实现等内容,都必须写到一个.h文件中去,其他的要用到类模板的源程序文件(如.cpp文件),只要#include这个类模板的.h文件即可。
类模板的成员函数是有模板参数的:
//.h文件
template
class TEST {
public:
void Test();
};
//.cpp文件
template
void TEST::Test()
{
}
一个类模板虽然里面可能有很多成员函数,但是,当实例化模板之后,如果后续没有使用到某个成员函数,则这个成员函数是不会被实例化的。换句话说,一个实例化的模板,它的成员只有在使用的时候才会被实例化。
普通的类成员函数只能在特定的类类型中使用,它们的实现代码在编译期间被实例化,并且只能处理特定的类型。而类模板成员函数可以在任意类型的模板实例中使用,它们的实现代码在模板实例化时被实例化,并且可以处理任意类型。
在类模板内部,可以直接使用类模板名,并不需要在类模板名后跟模板参数。因为在类模板定义内部,如果没提供类模板参数,编译器会假定类模板名带与不带模板参数等价(也就是TEST等价于TEST<T>)。
//.h中
template
class TEST {
public:
typedef T* iter;
iter GetNext();
};
//.cpp中
template
typename TEST::iter TEST::GetNext() //没有typename会报:语法错误: 标识符“iter”
{
return iter();
}
C++假定通过作用域运算符访问的是静态成员变量而不是类型,所以,上述代码中如果不加typename来修饰,编译器会报错。解决办法就是显式地告诉编译器iter是一个类型,所以在其前面用typename来修饰。
如果某个模板参数有默认值,那么从这个有默认值的模板参数开始,后面的所有模板参数都得有默认值(这一点和函数的形参默认值规则一样)。调用的时候,如果完全用默认值,则可以直接使用一个空的尖括号(空的尖括号不能省)。
//.h文件
template
class TEST {
public:
void Test();
};
//.cpp文件
template
void TEST::Test()
{
}
int main()
{
TEST t_float;
t_float.Test();
TEST<> t_int; //使用默认类型,<>不能省
t_int.Test();
return 0;
}
老的C++标准只允许为类模板提供默认模板参数,C++11新标准也可以为函数模板提供默认模板参数。
template
T Add(T a,T b)
{
return a + b;
}
不管一个普通类,还是一个类模板,它的成员函数本身可以是一个函数模板,这种成员函数称为“成员函数模板”,但是这种成员函数模板不可以是虚函数,如果写一个虚函数模板,编译器就会报错。
类模板,也是可以为它定义成员函数模板的,这种情况就是类模板和其成员函数模板都有各自独立的模板参数。
//.h文件
template
class TEST {
public:
template
void Test(); //类模板中的成员函数模板
void Test2(); //类模板中的成员函数
};
//.cpp中
template
template
void TEST::Test()
{
}
template
void TEST::Test2()
{
}
模板只有被使用时才会被实例化。所以,如果在两个不同的cpp文件中都使用了同一个模板,那么这个模板会被实例化两次。(这是因为C++的编译模型是单独编译每个源文件,每个源文件都有自己的编译上下文。当编译器在一个源文件中遇到模板使用时,它会在那个上下文中实例化模板。当它在另一个源文件中再次遇到同一个模板使用时,它会再次实例化模板。)
可以通过“显式实例化”来避免这种生成多个相同类模板实例的开销。模板的实例化定义只有一个,模板的实例化声明可以有多个。实例化定义不要忘记写,否则就达不到减少系统额外开销的效果或者会造成链接出错。
模板实例化定义的格式是以template开头,而模板实例化声明的格式是以extern template开头。当编译器遇到extern模板实例化声明时,就不会在本.cpp源文件中生成一个extern后面所表示的类模板的实例化版本代码。这个extern的意思就是告诉编译器,在其他的.cpp源文件中已经有一个该类模板的实例化版本了,所以这个extern一般写在多个.cpp源文件的文件开头位置。
//类模板
template
class TEST {
public:
void Test2(); //类模板中的成员函数
};
template
void TEST::Test2()
{
}
//_1.cpp中实例化定义
template TEST;//实例化定义
//_2.cpp中声明
extern template TEST;//实例化声明
template
T Add(T a,T b)
{
return a + b;
}
//_1.cpp
template int Add(int a, int b);//实例化定义
//_2.cpp
extern template int Add(int a, int b);//实例化声明
使用Visual Studio 2017或者Visual Studio 2019时,不推荐使用类模板显式实例化特色,因为该特色虽然有作用但也会把所有成员函数都实例化出来,增加了编译时间和代码长度。
typedef这种定义类型的顺序,就好像定义一个变量一样,typedef后面先写上系统的类型名,然后接一个空格,再接自己要起的类型别名。
typedef int INT;
INT a = 10;
以往已经看到过多次using的不同用法,如用它来暴露子类中同名的父类函数、子类继承父类的构造函数,今天再介绍个using的作用:别名模板。
//类模板
template
class TestUsingClass {
public:
void Test() {
}
};
//using测试
template
using MyClass = TestUsingClass;
void test22()
{
MyClass myclass;
myclass.Test();
}
using也可以定义普通类型,但在语法格式上,正好和typedef定义类型的顺序相反。
typedef int INT;
INT a = 10;
using INT = int;
INT a = 10;
int Fun(int a, int b)
{
return a * b;
}
typedef int(*MyFun)(int, int);
using MyFun = int(*)(int, int);
void Test(){
MyFun myFun = &Fun;
int result = myFun(1, 2);
std::cout << result << std::endl;
}
模板在用的时候可以为其指定任意的模板参数,这就叫泛化(更宽广的范围)。
写一个类模板或者函数模板,传递进去一个类型模板参数。这个传递进去的类型可以自己指定,但是存在这样一种情况,给进去一个A类型,这个模板能够正常实例化,但给进去一个B类型,这个模板就无法正常实例化,如编译报错等,这个模板就叫特化。
只要涉及特化,一定先存在泛化。直接写一个模板的特化版本而没有提供相应的通用(泛化)版本,编译器将会报错。因为特化版本实际上是对泛型模板进行了一种特殊情况下的重定义或者说覆盖。如果不存在这个泛型模板,那么就没有什么可以被特化或者覆盖。
全特化,也就是把所有类型模板参数都用具体类型代表。
// 函数模板声明
template
void foo(T t);
// 函数模板定义
template
void foo(T t) {
std::cout << "General template. Value: " << t << '\n';
}
// 函数模板全特化:对int类型进行特化
template<>
void foo(int i) {
std::cout << "Specialization for int. Value: " << i * 2 << '\n';
}
template <> 表示这是一个全特化,并且后面跟着要被全特化版本替换掉的原始模版定义。
函数模板全特化实际上等价于实例化一个函数模板,并不等价于一个函数重载。
// 通用的函数模板
template
void print(T value)
{
std::cout << "This is the general template: " << value << std::endl;
}
// 对int类型进行全特化
template <>
void print(int value)
{
std::cout << "This is the specialization for int: " << value * 2 << std::endl;
}
//普通函数
void print(int value)
{
std::cout << "This is a normal function: " << value * 2 << std::endl;
}
遇到一个函数调用,选择普通函数也合适,选择函数模板(泛化)也合适,选择函数模板特化版本也合适的时候,编译器考虑顺序是最优先选择普通函数,没有普通函数,才会考虑函数模板的特化版本,如果没有特化版本或者特化版本都不合适,才会考虑函数模板的泛化版本。
偏特化,也叫局部特化。这里要从两个方面说,一个是模板参数数量上的偏特化(这个好理解,就是部分参数特化),一个是模板参数范围上的偏特化。
函数模板不能偏特化。只有类模板才能偏特化。
模板参数范围上的偏特化:例如原来是int类型,如果变成const int类型,是不是这个类型的范围上就变小了!再如,如果原来是任意类型T,现在变成T*(从任意类型缩小为指针类型),那这个类型从范围上也是变小了!还有T&(左值引用)、T&&(右值引用)针对T来说,从类型范围上都属于变小了。