泛型编程(generic progamming)
假设想写一个交换函数,将第一个和第二个对象进行交换,由于不确定要比较数据的类型,所以可以针对不同类型写出所有函数的重载。如果这样做,会发现这些函数除了针对的类型不同,其他地方都相同,一个个实现这些函数未免麻烦并且容易出错。此外,如果希望将函数用于未知的自定义类型,这种方式就不起作用了。泛型编程可以完美解决这个问题。
泛型编程是一种编程风格,其中算法以尽可能抽象的方式编写,而不依赖于将在其上执行这些算法的数据形式。泛型编程使这些算法能够在不损失效率的前提下,能够被运用到最通用的环境中。在C++中,模板是泛型编程的基础。
模板(template)
模板是泛型编程的基础,是创建泛型类和泛型函数的蓝图。本文针对模板进行第一层次的总结,针对模板更高级的特性的讨论将在后续文章中进行。在C++中,使用 template<typename T1, typename T2. . .>
定义单个或多个模板参数,其中T是类型;typename 可用 class 替换。
template<typename T> //定义单个模板参数
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
template<typename T1, typename T2> //定义多个模板参数
T1 Add(T1 a, T2 b)
{
return a + b;
}
函数模板
函数模板代表了一个函数族,函数模板与类型无关,在使用时被实例化,针对类型实参或类型要求生成特定的函数类型版本。
函数模板是一个蓝图,其本身并不是函数,是编译器产生特定具体类型函数的模具。当看到模板定义时,编译器不立即生成函数代码,而是在使用到模板时,根据实际类型生成针对特定类型的函数。在编译阶段,对于函数模板的使用,编译器会根据传入的类型实参自动推演生成对应类型的函数代码以供调用,这个过程被称为函数模板的实例化。模板使用时不进行类型检查,也不保证类型安全。
函数模板的实例化
函数模板参数的实例化有两种方式:隐式实例化和显式实例化。
隐式实例化
隐式实例化即是上文所说明的,编译器根据类型实参自动推演模板参数的实际类型,并生成对应的函数。这种方式适用于绝大多数情况,但是有时会存在一些问题:
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int i1 = 10;
int i2 = 20;
double d1 = 3.14;
double d2 = 5.12;
Add(i1, i2);
Add(d1, d2);
// Add(i1, d2);
return 0;
}
注意到第 16 行的代码无法通过编译,这是因为编译器根据i1
推演的 T 类型为 int,而根据 d2
推演的 T 类型为 double,但是模板参数列表中只有一个T,这样便会造成歧义。此时有两种解决方案,第一种是将其中一个形参进行强制类型转换,另一种解决方案便是进行显式实例化。
显式实例化
显式实例化即是用户自行指定类型参数 T 的实际类型,如果参数的实际类型不匹配,编译器会进行强制类型转换,如果无法转换成功,则会报错。
template<typename T>
T Add(const T& a, const T& b)
{
return a + b;
}
int main()
{
int i1 = 10;
int i2 = 20;
double d1 = 3.14;
double d2 = 5.12;
Add(i1, i2);
Add(d1, d2);
// Add(i1, d2);
Add((double)i1, d2); //强制类型转换
Add<double>(i1, d2); //显式实例化
return 0;
}
函数模板参数的匹配原则
函数模板参数具有以下匹配原则:
- 一个非模板函数可以与一个同名的函数模板同时存在,且该函数模板可以被实例化为这个非模板函数。
template<typename T> //定义单个模板参数
void Swap(T& a, T& b)
{
T tmp = a;
a = b;
b = tmp;
}
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int i1 = 10, i2 = 20;
Swap(i1, i2); //与非模板参数类型匹配,编译器不需要进行实例化
Swap<int>(i1, i2); //显式实例化,调用实例化后的模板函数
return 0;
}
- 对于非模板函数与同名函数模板,在其他条件均相同的情况下,在调用时会优先调用非模板函数而不会进行函数模板实例化;如果模板可以实例化出一个参数更匹配的函数,则会选择模板。
template<typename T1, typename T2>
T1 Add(const T1& a, const T2& b)
{
return a + b;
}
int Add(const int& a, const int& b)
{
return a + b;
}
int main()
{
int i1 = 10;
int i2 = 20;
double d1 = 3.14;
double d2 = 5.12;
//与非模板函数类型完全匹配,直接调用非模板函数而不需要函数模板实例化
Add(i1, i2);
//函数模板可以生成类型更加匹配的函数,调用实例化出的函数
Add(i1, d2);
return 0;
}
类模板
类模板的定义与函数模板相似。类模板代表了一个类族,不是具体的类,是编译器根据欲被实例化的类生成具体类的模具。类模板同样支持定义多个模板参数。
template<class T1, class T2, ..., class Tn>
class className
{
// 类内成员定义
};
类模板的实例化
与函数模板不同,由于编译器无法预先判断类针对的具体类型,所以类模板只支持显式实例化。需要注意的是,类模板的模板名不是真正的类,实例化的结果才是真正的类。并且规定:对于普通类,类名与类型名相同;对于模板类,类名与类型名不同。
/*
类的名称为 Vector
类的类型为
template<typename T>
class Vector
*/
template<typename T>
class Vector
{
private:
T* _pData;
size_t _size;
size_t _capacity;
public:
Vector(size_t capacity = 10)
:_pData(new[capacity]),
_size(0),
_capacity(capacity)
{ }
/*...*/
T& operator[](size_t pos)
{
assert(pos < _size);
return _pData[pos];
}
~Vector()
{
if (_pData) {
delete[] _pData;
}
_size = _capacity = 0;
}
};