目录
一、必须使用 typename 的场景
二、非类型模板参数
三、模板的特化
3.1 - 函数模板特化
3.2 - 类模板特化
3.2.1 - 全特化
3.2.2 - 偏特化
四、类模板分离式编译
4.1 - 分离编译的概念
4.2 - 类模板分离式的问题
4.3 - 解决方案
一、必须使用 typename 的场景
问题:当我们定义了一个函数模板 Print,用于输出容器中的所有元素,编译却不能通过?
template<class Container>
void Print(const Container& ctnr)
{
Container::const_iterator it = ctnr.begin();
while (it != ctnr.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
发生编译错误是因为在模板实例化之前,编译器无法确定 Container::const_iterator
是嵌套类型,还是静态成员变量,亦或是静态成员函数。
因此,需要使用 typename 关键字直接告诉编译器 Container::const_iterator
是一个类型:
typename Container::const_iterator it = ctnr.begin();
除此之外,还有另一种解决办法,即:
auto it = ctnr.begin();
其他应用场景:
template <class T, class Container = vector<T>,
class Compare = less<typename Container::value_type> > class priority_queue;
二、非类型模板参数
对于函数模板和类模板,模板参数并不局限于类型,普通值也可以作为模板参数,即非类型模板参数(Nontype Template Parameters)。
例如 C++11 标准中新增的序列容器 array,它以类模板的形式定义在 <array> 头文件中,并位于 std 命名空间中。
template <class T, size_t T > class array;
其中 N 就是非类型的类模板参数,用于指定容器的大小。
需要注意的是,非类型模板参数有一定的限制,浮点数、类对象以及字符串是不允许作为非类型模板参数的。
三、模板的特化
通常情况下,使用模板可以实现与类型无关的代码,但对于一些特殊类型,可能会得到非预期的结果,此时就需要对模板进行特化,即在原模板的基础上,针对特殊类型做特殊化处理。
模板的特化有时也被称为模板的具体化,可分为函数模板特化和类模板特化。
3.1 - 函数模板特化
#include <iostream>
using namespace std;
// 函数模板
template<class T>
T Max(const T x, const T y)
{
return x > y ? x : y;
}
// 对 Max 函数模板进行特化
template<>
const char* Max<const char*>(const char* x, const char* y)
{
return strcmp(x, y) > 0 ? x : y;
}
int main()
{
cout << Max(10, 20) << endl; // 20
cout << Max('a', 'z') << endl; // z
cout << Max("abcdefg", "hijk"); // hijk
return 0;
}
如果不对 Max 函数模板进行特化,那么在比较字符串 "abcdefg"
和 "hijk"
的大小时,比较的是两个字符串的起始地址的大小,而不是字符串的内容。
除了定义函数模板的特化版本,还可以直接给出对应的普通函数,即:
const char* Max(const char* x, const char* y)
{
return strcmp(x, y) > 0 ? x : y;
}
程序运行的结果和使用函数模板特化相同,但是如果使用普通函数,那么不管是否发生函数调用,都会在目标文件中生成该函数的二进制代码,而如果使用模板函数的特化版本,除非发生函数调用,否则不会在目标文件中生成特化模板函数的二进制代码,这符合函数模板的 "惰性实例化" 准则。
3.2 - 类模板特化
3.2.1 - 全特化
全特化即将模板参数列表中所有的参数都确定化。
下面是一个用于比较的类模板,里面可以有多种用于比较的函数,以 IsEqual
为例。
#include <cstdlib>
#include <iostream>
using namespace std;
template<class T>
class Compare
{
public:
static bool IsEqual(const T& lhs, const T& rhs)
{
return lhs == rhs;
}
};
// specialize for double
template<>
class Compare<double>
{
public:
static bool IsEqual(const double& lhs, const double& rhs)
{
return abs(lhs - rhs) < 10e-6;
}
};
int main()
{
double epsilon = 0.001;
double d1 = 2.234;
double d2 = 2.235;
if (Compare<double>::IsEqual(d2 - d1, epsilon))
cout << "equal" << endl;
else
cout << "unequal" << endl;
return 0;
}
因为浮点数在计算机中的存储并不总是精确的,所以在判断两个浮点数是否相等时,应该采用比较两数之差的绝对值是否小于一个很小的数字来确定是否相等的方法。
3.2.2 - 偏特化
偏特化有以下两种表现方式:
-
部分特化,即将模板参数的一部分参数特化。
#include <iostream> using namespace std; template<class T1, class T2> class A { public: A() { cout << "A<T1, T2>" << endl; } private: T1 _i; T2 _j; }; // 部分特化 template<class T1> class A<T1, int> { public: A() { cout << "A<T1, int>" << endl; } private: T1 _i; int _j; }; int main() { A<double, double> a1; // A<T1, T2> A<double, int> a2; // A<T1, int> return 0; }
-
对参数做更进一步的限制。
例一:
template<class T> class Compare<T*> { public: static bool IsEqual(const T* lhs, const T* rhs) { return Compare<T>::IsEqual(*lhs, *rhs); } };
这种特化不是一种绝对的特化,它只是对类型做了某些限定,但仍然保留了其一定的模板性,这种特化给我们提供了极大的方便,如这里,我们就不再需要对
int*、float*、double*
等等类型分别做特化了。例二:
template<class T> class Compare<vector<T>> { static bool IsEqual(const vector<T>& lhs, const vector<T>& rhs) { if (lhs.size() != rhs.size()) return false; for (size_t i = 0; i < lhs.size(); ++i) { if (lhs[i] != rhs[i]) return false; } return true; } };
这里把
IsEqual
的参数限定为一种 vector 类型,但具体是 vector<int> 还是 vector<float>,我们可以不关心,因为对于这两种类型,我们的处理方式都是一样的。
四、类模板分离式编译
4.1 - 分离编译的概念
一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过程称为分离式编译模式。
4.2 - 类模板分离式的问题
A.h
:
#pragma once
template<class T>
class A
{
public:
void f(); // 声明
};
A.cpp
:
#include "A.h"
// 定义
template<class T>
void A<T>::f()
{
// ... ...
}
main.cpp
:
#include "A.h"
int main()
{
A<int> a;
a.f();
return 0;
}
这是因为 A.cpp
中的函数模板代码不能直接编译成二进制代码,其中需要一个实例化的过程,所以最终出现的链接错误。
4.3 - 解决方案
-
在模板定义的位置显示实例化,即:
#include "A.h" template class A<int>; // 显示实例化 // 定义 template<class T> void A<T>::f() { // ... ... }
但是这种方法不实用,不推荐使用,推荐使用下面那种方法。
-
将声明和定义放在一个
.hpp
或者.h
文件中,即:#pragma once template<class T> class A { public: void f(); // 声明 }; // 定义 template<class T> void A<T>::f() { // ... ... }