一、非类型模板参数
模板参数,分为类型形参和非类型形参。
类型形参就是在模板中跟在typename和class之后的参数类型名称,非类型形参就是用一个常量作为类模板或者函数模板的一个参数,在类模板和函数模板中,可以将该参数当作常量来使用。
例如,现在要实现一个静态栈:
using namespace std;
#include <iostream>
#define N 10
template <typename T>
class Stack
{
//......
private:
T _a[N];
int _top;
};
int main()
{
Stack<int> st1;
Stack<int> st2;
return 0;
}
问题是,这个栈的大小只有10,因为先前就在宏定义中将N定义为了10。所以不能根据需求对栈进行实例化。
那么现在就可以用到非类型模板参数了,可以这样来实现栈:
using namespace std;
#include <iostream>
template <typename T,size_t N>
class Stack
{
//......
private:
T _a[N];
int _top;
};
int main()
{
Stack<int,10> st1;
Stack<int,100> st2;
return 0;
}
从监视1窗口可以看到,st1和st2大小分别为10和100:
除此之外,应该注意浮点数、类对象以及字符串是不允许作为非类型模板参数的;非类型的模板参数必须在编译期就能确认结果。
二、模板的特化
先来看几个例子。
定义一个函数模板如下:
template<class T>
bool Less(T left,T right)
{
return left < right;
}
测试1:
int main()
{
int num0 = 0;
int num1 = 1;
bool res = Less(num0,num1);
return 0;
}
可以正常运行。
测试2:
int main()
{
double num0 = 0;
double num1 = 1;
bool res = Less(num0, num1);
return 0;
}
可以正常运行。
测试3,这次让指针进行比较:
int main()
{
double num0 = 0;
double num1 = 1;
double* pnum0 = &num0;
double* pnum1 = &num1;
bool res = Less(pnum0, pnum1);
return 0;
}
根据监视1窗口的结果,可以看到返回值为false。事实上,num0的值是小于num1的值的,返回值为true才对。但是这里是对指针进行大小比较,这是一种没有意义的比较。
因此就要用特化来处理。
特化即在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。
模板特化中分为函数模板特化与类模板特化。
函数模板特化的步骤:
1. 必须要先有一个基础的函数模板
2. 关键字template后面接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
针对Less模板,当参数为指针时,应该这样特化:
template<class T>
bool Less(T left,T right)
{
return left < right;
}
template<>
bool Less<double*>(double* left, double* right)
{
return *left < *right;
}
这次调用,就会发现返回值正常了:
不过应注意,一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。
比如,刚才对double*类型进行特化,其实完全可以写成如下代码:
bool Less(double* left, double* right)
{
return *left < *right;
}
该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化。
上述内容是函数模板的特化,接下来还有类模板的特化。
类模板的特化分为全特化和偏特化。
全特化即是将模板参数列表中所有的参数都确定化,比如:
template<class T1,class T2>
class Data
{
public:
Data()
{
cout << "template<class T1,class T2>" << endl;
}
private:
T1 t1;
T2 t2;
};
template<>
class Data<int, double>
{
public:
Data()
{
cout << "template<>" << endl;
}
private:
int t1;
int t2;
};
偏特化又分为部分特化和参数的进一步限制,部分特化如下:
template<class T1, class T2>
class Data
{
public:
Data() { cout << "Data<T1, T2>" << endl; }
private:
T1 _d1;
T2 _d2;
};
template <class T1>
class Data<T1, int>
{
public:
Data() { cout << "Data<T1, int>" << endl; }
private:
T1 _d1;
int _d2;
};
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
这是将两个参数特化为指针:
template <typename T1, typename T2>
class Data <T1*, T2*>
{
public:
Data() { cout << "Data<T1*, T2*>" << endl; }
private:
T1 _d1;
T2 _d2;
};
这是将两个参数特化为引用:
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public:
Data(const T1& d1, const T2& d2)
: _d1(d1)
, _d2(d2)
{
cout << "Data<T1&, T2&>" << endl;
}
private:
const T1& _d1;
const T2& _d2;
};
三、模板的分离编译
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离编译模式。
首先明确,模板要尽量避免分离编译!!!
这是因为,C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来。
比如:
//a.cpp
#include "a.h"
template <class T>
bool Less(T left, T right)
{
return left < right;
}
//a.h
template <class T>
bool Less(T left, T right);
//test.cpp
#include "a.h"
int main()
{
int num0 = 0;
int num1 = 1;
Less(num0,num1);
}
当调试的时候,会发现编译器会有如下提示:
先前已提到,当一个模板不被用到的时侯它就不该被实例化出来。
虽然说,在test.cpp中用到了Less函数,但是a.cpp中并没有用到cpp函数,也就是说它没有被实例化。
造成这一结果的原因是,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,在本例中,a.h的代码被扩展到a.cpp中,test.cpp和a.cpp都会被编译器生成为.obj文件。
正常情况下,编译器会将test.cpp中的Less看作外部链接,也就是会在其它的.obj文件中查找Less函数的二进制地址,去调用。
然而,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“实例化”的过程!!!
所以,为避免这种情况发生,可以采取以下两种方法:
1.将声明和定义放在同一个.h或.hpp文件中;
如:
//a.h
template<class T>
T Add(T left, T right);
template<class T>
T Add(T left, T right)
{
return left + right;
}
//test.cpp
#include "a.h"
int main()
{
int num0 = 0;
int num1 = 1;
int res = Add(num0,num1);
}
2.模板定义的位置显式实例化,但是不推荐这种用法。
//a.cpp
template<class T>
T Add(T left, T right)
{
return left + right;
}
template
int Add<int>(int, int);
//a.h
template<class T>
T Add(T left, T right);
//test.cpp
#include "a.h"
int main()
{
int num0 = 0;
int num1 = 1;
int res = Add(num0,num1);
}