前言
模板是 C++ 中的泛型编程的基础。 作为强类型语言,C++ 要求所有变量都具有特定类型,由程序员显式声明或编译器推导。 但是,许多数据结构和算法无论在哪种类型上操作,看起来都是相同的。 使用模板可以定义类或函数的操作,并让用户指定这些操作应处理的具体类型。
在C++中模板有函数模板与类模板。
模板定义
template<typename/class T>
模板参数的数量没有实际限制。 以逗号分隔多个参数:template<typename T,typename T2>
可以使用省略号运算符 (...) 定义采用任意数量的零个或多个参数的模板:
template<typename... Arguments>
模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
每一个函数或类要进行模板化时,无论是在声明还是定义处,都要在函数和类的每一处声明和定义前定义模板,即在每一次声明和定义前指定template<...>语句
。
函数模板
可以定义如下所示的函数模板:
template<typename T>
void PRINTF_SEQUENCE(seq_LinkList<T> seq)
{
int length=seq.getlength();
for (int i = 0; i < length; i++)
{
cout<<seq.get(i)<<endl;
}
}
上面的代码描述了一个具有单个类型参数 T 的泛型函数的模板,其调用参数具有此类型。 可以随意命名类型参数,但按照约定,最常使用单个大写字母。 T 是模板参数;关键字 typename
表示此参数是类型的占位符,也可使用class关键字声明,即template<typename/class T>
。 调用函数时,编译器会将每个 T
实例替换为由用户指定或编译器推导的具体类型参数。 编译器从模板生成类或函数的过程称为“模板实例化”;如PRINTF_SEQUENCE
<int>
是模板 PRINTF_SEQUENCE
<T>
的实例化。
可以显示声明函数的泛型。PRINTF_SEQUENCE
由于这是一个函数模板,编译器可以从参数 a 和 b 中推导出类型,因此可以像普通函数一样调用它:T
seq_LinkList<int> seq;
seq.ADD(11);
seq.ADD(12);
seq.ADD(13);
PRINTF_SEQUENCE(seq);
原理:当编译器遇到最后一个语句时,它会生成一个新函数,在该函数中,T 在模板中的每个匹配项都替换为 int
:
即生成一个新函数,此函数的定义与声明对用户透明。
void PRINTF_SEQUENCE(seq_LinkList<int> seq)
{
int length=seq.getlength();
for (int i = 0; i < length; i++)
{
cout<<seq.get(i)<<endl;
}
}
如程序中其他页面定义了其他类型的形参,即出现了新的模板实例。如定义如下代码行seq_LinkList
编译器编译后又会生成一个新的函数
PRINTF_SEQUENCE(seq);void PRINTF_SEQUENCE(seq_LinkList
类型模板
类模板的格式:
template <class T>
class 类名
{
// 类定义
}
可以使用省略号运算符 (...) 定义采用任意数量的零个或多个类型参数的模板:
template<typename... Arguments> class vtclass;
vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;
在类的每一处声明和定义前都要定义模板,即在每一次声明和定义前指定template<...>语句
。
包括在定义模板类的继承以及使用::操作符指定模板类的作用域时也要在前定义模板语句。
子类stack_LinkList
是本身是模板类,同时也要继承父类模板类型。
template<typename ElementType>
class stack_LinkList:public istack<ElementType>
{
private:
...
public:
stack_LinkList(/* args */):istack<ElementType>()//同时调用父类的构造函数
{
top=new LinkNode();
length=0;
}
};
如果子类不是模板类,需要指明父类的具体类型。
class stack_LinkList:public istack<int>
{
private:
...
public:
stack_LinkList(/* args */):istack<int>()
{
top=new LinkNode();
length=0;
}
};
在类内声明类函数,类外定义类函数时,指定函数所属类的同时,也要指定类属于哪个具体实例模板。正如我们前面所说编译器在生成具体模板的过程叫做模板的实例化过程,即生成一个具体的类或函数。所以我们在指定函数所属类同时也要在前面定义模板。
template<typename ElementType>
class stack_LinkList:public istack<ElementType>
{
private:
...
public:
stack_LinkList(/* args */):istack<ElementType>()
{
top=new LinkNode();
length=0;
}
void Push(ElementType value);
ElementType Pop();
ElementType Top();
int getLength();
bool isEmpty();
};
template<typename ElementType>
bool stack_LinkList<ElementType>::isEmpty()
{
return top->next==nullptr;
}
....
非类型参数
与其他语言(如 C# 和 Java)中的泛型类型不同,C++ 模板支持非类型参数,也称为值参数。 例如,可以提供常量整型值来指定数组的长度,例如在以下示例:
template<typename T, int L>
class Array
{
T arr[L];
public:
Array() { ... }
};
在 Visual Studio 2017 及更高版本中,在 /std:c++17
模式或更高版本中,编译器会推导使用 auto
声明的非类型模板参数的类型:
template <auto x> constexpr auto constant = x;
auto v1 = constant<5>; // v1 == 5, decltype(v1) is int
auto v2 = constant<true>; // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>; // v3 == 'a', decltype(v3) is char
模板特殊化
可以为该特定类型定义模板的专用化。 当用户使用该类型对模板进行实例化时,编译器使用该专用化来生成类,而对于所有其他类型,编译器会选择更常规的模板。 如果专用化中的所有参数都是专用的,则称为“完整专用化”或"全特化"。 如果只有一些参数是专用的,则称为“部分专用化”。(将模板参数中的一部分参数具体指定)
全特化
//全特化
template<>
class data <int, char>
{
public:
data(int a, char b)
:_a(a)
, _b(b)
{
cout << "全特化data<int, char>" << endl;
}
private:
int _a;
char _b;
};
int main()
{
data<int, int> s1(1, 2);
data<int, char> s2(1, 'a');
}
部分特化
// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization