STL
的一些类模板),就会很明显发现泛型不仅仅是类型的问题,例如:“适配器”的使用(在后面双端队列里有体现),实际上就是一种泛型,对于泛型的理解我们不能仅限于类型。
模板除了类型模板,还有非类型模板。
类型模板:出现在模板的参数列表中,跟在class
或者typname
后的参数类型名称
非类型模板:使用一个常量作为类的一个非类型模板参数,在模板类/模板函数中可以将该参数作为常量来使用,且不能修改。并且,这里非类型模板参数也可以使用缺省值
//没有非类型模板参数
#include
using namespace std;
#define NUM 10
template<class T>
class Data
{
public:
//...
private:
T _arr[NUM];
};
int main()
{
Data<int> a1;
//无法修改初始化大小(注意是初始化的时候修改大小)
//只能手动调整#define的值
//和之前的typedef的问题类似
Data<double> a2;
}
这个时候就可以使用非类型模板参数,这个参数是一个常量,更加准确来说是不可被修改的整形常量(包括布尔类型)。
//有非类型模板参数
#include
using namespace std;
//#define NUM 10
template
class Data
{
public:
//...
private:
T _arr[N];
};
int main()
{
Data a1;//默认初始化申请50个空间
Data a2;//初始化时申请20个空间
}
您可能会疑惑:为什么不可以初始化先使用new
开辟固定的空间,等到后续操作进行扩容操作呢?注意这里只是利用这个例子来简述语法特性,并不是实际的用途(在后续“位图”等知识中有很大的价值)。
补充:除了使用这个常量,还可以将这个常量作为一个类的标识数字来使用。
函数模板也可以使用这一特性。
#include
using namespace std;
template<class T, size_t N = 50>
class Data
{
public:
//...
public:
T _arr[N];
};
template<class T, long NUM = 50>//演示了其他整形
void function(T& i)
{
i = NUM;
}
int main()
{
Data<int, 10> a1;
Data<int, 100> a2;
int i = 0;
function<int, 200>(i);//演示了函数修改非类型模板参数
cout << i << endl;
}
C++ 11
搞的新容器:静态数组array
,其类模板就是使用了这个非类型模板参数。
#include
#include
using namespace std;
int main()
{
array<int, 10>arr;
for (auto &i : arr)
{
i = 10;
}
for (auto i : arr)
{
cout << i << " ";
}
cout << endl;
return 0;
}
可惜静态数组不会进行初始化(吐槽:std::array
当参数传递仍然要把数组的长度传过去,挺好玩的…),也支持范围for
,并且越界检查比较严格(传统数组是抽查,但是静态数组是读写越界全面检查,避免代码崩溃)。
嘛…感觉优势不算很大(大不了使用vector
,这也可以查找越界,还可以使用列表初始化)所以推广并不高。这个容器有点为了强迫症而统一STL
风格的感觉。
类似deque
在list
和vector
的感觉(后面会讲),静态数组就是传统数组和vector
之间的方案。
通常模板可以实现和类型无关的代码,但是有一些特殊的类型可能会得到一些错误的、不符合预期的结果,因此需要进行特殊处理,这就有了“模板特化”这个概念。
#include
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data()
{
cout << "Data" << endl;
}
private:
T1 _d1;
T2 _d2;
};
template<>//全特化,必须要写这句
class Data<int, char>//这里指定了特定的类型
{
public:
Data()
{
cout << "Data" << endl;
}
private:
int _d1;
char _d2;
};
void TestVector()
{
Data<int, int> d1;
Data<int, char> d2;//这样就会直接调用全特化的模板,不会再去类模板构造
}
int main()
{
TestVector();
}
除了全特化,还可以进行偏特化。在下述代码中,我们可以看到偏特化不仅只是做了一些类型的指定,也可以对类型做进一步限制。
#include
using namespace std;
template<class T1, class T2>
class Data
{
public:
Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2)
{ cout << "Data" << endl; }
private:
T1 _d1;
T2 _d2;
};
//1.部分类模板参数特化
template <class T1>
class Data<T1, int>
{
public:
Data(const T1& d1, const int& d2) : _d1(d1), _d2(d2)
{ cout << "Data" << endl; }
private:
T1 _d1;
int _d2;
};
//2.1.对两个参数进行进一步限制,偏特化为指针类型
template <typename T1, typename T2>//这里也是必须写,和全特化有些不同
class Data <T1*, T2*>
{
public:
Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2)
{ cout << "Data" << endl; }
private:
T1 _d1;//注意其成员不是指针,仍然是原类型
T2 _d2;//注意其成员不是指针,仍然是原类型
};
//2.2.对两个参数进行进一步限制,偏特化为引用类型
template <typename T1, typename T2>//这里也是必须写,和全特化有些不同
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2) : _d1(d1), _d2(d2)
{cout << "Data" << endl; }
private:
T1 _d1;
T2 _d2;
};
void test()
{
Data<int, double> d1(10, 20);//调用基础的类模板
Data<int, int> d2(30, 40);//调用偏特化的类模板
Data<int*, int*> d3(1, 2);//调用偏特化的指针版本
Data<int&, int&> d4(3, 4);//调用偏特化的引用版本
}
int main()
{
test();
return 0;
}
补充:偏特化会使得特化更加强大,某些程度上来说比全特化更加常用。
因此可以总结类模板的特化语法就是:
//1.原类模板
template<class T1, class T2, /*...*/, class Tn>
class ClassName
{/*...*/};
//2.特化类模板
template</*填入仍旧继续使用的泛型(如果都使用可以省略这里)*/>
class ClassName</*指定特定的类型,并且写入仍旧使用的泛型,注意顺序*/>
{/*...*/};
#include
using namespace std;
//类模板
class Data
{
public:
Data(int d) : _d(d) {}
bool operator<(const Data& x)
{ return _d < x._d; }
private:
int _d;
};
//函数模板
template<class T>
bool Less(T left, T right)
{
return left < right;
}
//特化函数模板
template<>
bool Less<Data*>(Data* left, Data* right)
{
return (*left) < (*right);
}
/*
template<>
bool Less(const Data* & left, const Data* & right)//这种写法很特殊,是没有办法通过的,原本是为了使用const修饰引用变量,避免引用变量被修改,但是由于指针和const修饰的特殊性,导致const修饰了*,因此只能改成:(Data* const& left, Data* const& right)这种写法虽然奇怪,但是却是正确的。
{
return (*left) < (*right);
}
*/
int main()
{
cout << Less(1, 2) << endl;//调用了普通的函数模板
Data d1(1);
Data d2(2);
cout << Less(d1, d2) << endl;//调用了普通的函数模板
Data* p1 = &d1;
Data* p2 = &d2;
cout << Less(p1, p2) << endl;//调用特化后的函数模板,虽然这种调用看起来很奇怪
return 0;
}
注意
1
:区分好“匹配”和“特化”和“实例化”。
- 匹配:是有相匹配的类型,可以使用对应的模板
- 实例化:是编译器自己做的,将匹配对应的模板进行实例化
- 特化:特化不是全新的模板,必须依赖模板,不可以单独存在
注意
2
:实际上特化更加适合类模板一些,实际上函数重载(重载)对比函数模板特化(匹配)更加简单。
这一点凸显在函数的声明定义的分离上,假设有下面三个文件:
//function.h内声明
#pragma once
#include
template<class T>
T Add(const T& left, const T& right);
int NoTemplateAdd(const int& left, const int& right);
//function.cpp内定义
template<class T>
T Add(const T& left, const T& right)
{
return left + right;
}
int NoTemplateAdd(const int& left, const int& right)
{
return left + right;
}
//main.cpp内包含头文件并且调用
#include "function.h"
int main()
{
std::cout << Add(1, 2);//链接错误
std::cout << Add(1.0, 2.0);//链接错误
std::cout << NoTemplateAdd(1, 2);//成功调用
return 0;
}
可以发现函数模板没有办法声明和定义分离在两个文件中,会显示链接错误(但是普通的函数可以)。
让我们来分析一下这里面的原因:
C/C++
要运行程序,就需要经历“预处理-编译-汇编-链接”function.obj
或者说function.o
中,由于编译器没有看到函数的实例化,因此没有生成具体的加法函数。main.obj
或者main.o
中,编译器看到有加法函数的调用,但是暂时不知道具体的实现,因此就暂时放进了符号表里等待后续链接function.obj
或者说function.o
中没有加法函数的定义,根本就无法提供加法函数的地址在符号表里供main.obj
或者main.o
链接因此后续链接的时候就会报错,即“链接错误”。
如果一定要分离,有两种方法:
进行显示实例化(有缺陷)
//function.h
#include
using namespace std;
template <typename T>
void MyFunction(T value);
//function.cpp
#include "function.h"
template <typename T>
void MyFunction(T value)
{
cout << value << endl;
}
//显式实例化int类型的函数模板
template void MyFunction<int>(int value);
//main.cpp
#include "function.h"
int main()
{
//调用int版本的函数模板
MyFunction(42);
return 0;
}
在一个翻译单元里分离,即:干脆直接将定义和声明都写在一个.hpp
内,这样做是更加推荐的。