泛型编程与模板
实现一个简单的Swap交换函数 :
void Swap (int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
很明显上面的Swap函数 只能交换int类型 的数据,而实际中要交换的数据类型会有许多。不过C++支持函数重载,我们可以将所有的交换数据类型的Swap函数全重载出来:
void Swap (char& left, char& right)
{...}
void Swap (double& left, double& right)
{...}
如果要实现一个通用类型的Swap函数,采用函数重载就非常不可取了:
- 重载的函数仅仅只是类型不同,代码的复用率低;
- 实际中除了要交换C++的内置类型数据,还可能交换用户自定义类型的数据,如果事先不知道这个类型是什么,如何写出该类型的交换函数?
我们常用的C++各种库函数,比如STL库(标准模板库),里面涉及的都是通用函数,这些通用函数的实现就是利用了泛型编程。
泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模版是泛型编程的基础,使编程与类型无关
泛型 是指具有在多种数据类型上皆可操作的含义,与模板有些相似
STL包含了许多常用的算法和数据结构,但其内部实现均将算法与数据结构完全分离,其中算法是泛型的,不与任何特定数据结构或对象类型系在一起
==模版 != 泛型编程,模版可以做到与类型无关,不一定能做到代码通用;
但模版是泛型编程的基础,是实现泛型编程非常重要的一个环境; 模板的本质:类型参数化
为什么说模版 != 泛型编程? 举例:若要实现一个通用的查找方法find() 函数
template<typename T>
int find(const T& array[], size_t size, const T& data)
{
for (size_t i = 0; i < size; ++i)
{
if (array[i] == data)
{
return i;
}
}
return -1;
}
但这个find函数模版 真的是一个泛型方法吗?
如果我们要查找的数据不是在连续的空间(如数组),而是在链表、堆或二叉树中,那么这个方法根本无法实现功能;
因此这个find只是个函数模版,而不是泛型编程代码;C++的STL(标准模版库)大都是泛型编程,用到了许多特殊的方法来实现泛型编程(迭代器…),其中数据结构就是用模版来进行封装的;
模版有哪些
1. 函数模版
概念
函数模板代表了一个函数家族,该函数模板与类型无关。在使用时被参数化,根据实参类型产生函数的特定类型版本。
定义
-
定义模板参数列表
定义模板关键字template ; 定义模版参数关键字typname ,也可用class (早期的编译器只能用class) (注意这里不能用struct替换class)
template<typename T1, typename T2,......,typename Tn> 返回值类型 函数名(参数列表){} (T就是一个通用类型,可替换名字)
-
定义函数模板
函数定义时将需要替换的类型直接用通用类型即可。
例子:
- 一个Swap函数模版的例子:(模版类型只有一个)
template<class T>
void Swap (T& left, T& right)
{...}
- 一个多参数模版
template<class T1, class T2, typename T3>
void Fun(T1 a, T2 b, T3 c)
{
cout << a << endl;
cout << b << endl;
cout << c << endl;
}
- 函数模版的注意事项:
模版定义在主函数外; 模版参数无特殊情况最好用引用类型(效率高); 函数模版并不是真正的函数,只是编译器的一种编译规则;
实例化——函数模版的使用
揭示函数模版的原理:
- 函数模版并不是真正的函数,只是编译器的一种编译规则;
- 函数模版未实例化前,编译器不会生成具体类型的函数;
- 只有当该模版使用时被,才会实例化出对应类型的函数。
template<class T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 2.2) << endl;
cout << Add('a', 'b') << endl;
return 0;
}
通过反汇编代码查看模版的实例化情况:
函数模版实例化的分类:
-
隐式实例化——直接调用模版 当用户直接调用,编译器会根据实例化的结果来推演参数的类型,根据推演的结果生成具体处理该参数类型的实例化函数。 (上面的例子已经体现了函数模版隐式实例化的具体做法) 但是隐式实例化无法处理某些特殊情况: 解决方法:用户强转类型 或 采用显示实例化模版 -
显示实例化 ——函数名<类型> 编译器不需要对实参类型进行推演,直接按照<>内类型与模版自动生成相应函数。
几点规则
【思考】上面实现了一个Add()函数模版 ,可以实现任意类型的数据相加,但是当类型为char字符 时,调用该模版返回的值并不是我想要的
-
一个非模板函数可以和一个同名函数模板同时存在,且编译器优先调用已存在的非模版函数而不是模版,但该函数模板仍可以被显示实例化后调用 -
模板函数不允许自动类型转换,但普通函数可以进行自动类型转换 -
模版与同名函数同时存在,但使用时如果函数需类型转换,编译器会对比模版与函数,优先选择最匹配的方式
2. 类模版
概念
通过替换类中的类型为通用类型,让类变成模版类,这时类名就是模版类名。 类模板不同于函数模板的地方在于,编译器不能为类模板推断参数类型。
定义
例:实现一个简单的自定义顺序表模版
template <typename T>
class SeqList
{
private:
void CheckCapacity()
{
if (_size < _capacity)
{
return;
}
T* temp = new T[_capacity << 1];
memcpy(temp, _arr, _size*sizeof(T));
delete[] _arr;
_arr = temp;
_capacity *= 2;
}
public:
SeqList()
:_arr(new T[3])
, _size(0)
, _capacity(3)
{}
SeqList(T* arr, size_t size)
:_arr(new T[size])
, _size(size)
, _capacity(size)
{
for (size_t i = 0; i < size; ++i)
{
_arr[i] = arr[i];
}
}
SeqList(const SeqList& s)
: _arr(new T[s._size])
, _size(s._size)
, _capacity(s._size)
{
for (size_t i = 0; i < s._size; ++i)
{
_arr[i] = s._arr[i];
}
}
void Print()
{
if (_size == 0)
{
cout << "Print Error!" << endl;
return;
}
for (int i = 0; i < _size; i++)
{
cout << _arr[i] << " ";
}
cout << endl;
}
void PushBack(const T& data);
void PopBack()
{
if (_size == 0)
{
cout << "PopBack Error!" << endl;
return;
}
_size--;
}
template<typename U>
void Func(U a);
private:
T* _arr;
size_t _size;
size_t _capacity;
};
template<typename T>
void SeqList<T>::PushBack(const T& data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
template<typename T>
template<typename U>
void SeqList<T>::Func(U a)
{
cout << a << endl;
}
实例化
类模版只有显示实例化一种方法 例:实例上面的顺序表类模版
int main()
{
SeqList<int> s1;
s1.PushBack(1);
s1.PushBack(2);
s1.PopBack();
s1.Func<double>(1.1);
return 0;
}
注意事项
-
类模板名是一个模版名,并不是类型,不能用来实例化对象。要实例化对象需要用类模板名<具体类型> (相当于一个类)来进行实例化
元素可以为内置类型,比如: SeqList<int> 是一个元素为int的顺序表类、 SeqList<double> 是一个元素为double的顺序表类、 …
元素也可以是自定义类型:SeqList<Data> 是一个元素为Data对象的顺序表类;
-
类外定义成员函数或成员函数模版,记得添加类模版和函数模版的参数列表 -
类模板是一个类家族,模板类是通过类模板实例化的具体类
非类型模版参数
模版的实质就是类型参数化,对于函数模版或类模版的定义,第一步都是定义模版参数列表;
模板参数种类
上述的参数列表可以有两种模版参数:
- 类型形参:跟在
class 或typename 后的参数类型名称,可以是内置类型或自定义类型; - 非类型形参(非类型模版参数):用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
例:一个函数模版采用非类型参数
注意事项
-
类模版和函数模版均可采用非类型模版参数; -
非类型模版参数可以设为缺省参数; -
非类型模版参数本质是常量,函数内不可当成左值修改内容; -
浮点数、类对象以及字符串是不允许作为非类型模板参数; -
非类型的模板参数必须在编译期就能确认结果;
模版的特化
为什么要特化?
这是一个很简单的Max() 函数模版:
template <typename T>
T Max(T left, T right)
{
return left > right ? left : right;
}
使用该模版的实例化:
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的使用模版的实例化代码无法达到目的,出现结果错误。
为了解决这种问题,C++提供了模版特化这样的机制,函数模版和类模版都可以特化。
模版特化种类
1. 函数模版特化(不推荐使用)
特化步骤: 例:上述Max() 函数模版对于字符串类型的特化
template <typename T>
T Max(T left, T right)
{
return left > right ? left : right;
}
template<>
char* Max<char*>(char* left, char* right)
{
return strcmp(left, right) > 0 ? left : right;
}
为什么不推荐使用函数模版特化?
对于刚才的Max() 函数模版,我们不希望函数内部修改参数,为了代码的安全与高效,将其修改为:
template <typename T>
const T& Max(const T& left, const T& right)
{
return left > right ? left : right;
}
修改了函数模版,其特化理所当然也要改变,但是并不容易正确使用 而前面我们在函数模版的匹配规则说过,C++支持与函数模版同名的函数存在,使用模版同名函数也能处理上述问题,而且简单不易出错。因此我们推荐使用模版同名函数,而不是为函数模版提供特化
实际例子展示 :注意那个特殊的特化函数模版的例子
#include<iostream>
using namespace std;
template<typename Type>
const Type Max(const Type &a, const Type &b)
{
cout << "This is Max<Type>" << endl;
return a > b ? a : b;
}
template<>
const int Max<int>(const int &a, const int &b)
{
cout << "This is Max<int>" << endl;
return a > b ? a : b;
}
template<>
const char Max<char>(const char &a, const char &b)
{
cout << "This is Max<char>" << endl;
return a > b ? a : b;
}
template<>
const char*& Max<const char*&>(const char* &a, const char* &b)
{
cout << "This is Max<char>" << endl;
return a > b ? a : b;
}
int Max(const int &a, const int &b)
{
cout << "This is Max" << endl;
return a > b ? a : b;
}
int main()
{
Max(10, 20);
Max(12.34, 23.45);
Max('A', 'B');
Max<int>(20, 30);
return 0;
}
2. 类模版特化
特化步骤:大体与函数特化步骤相同
类模版特化分为:
全特化:将模板参数列表中所有的参数都确定化
template<class T1, class T2>
class Test
{
public:
Data()
{
cout << "调用Test类模版" << endl;
}
private:
T1 _d1;
T2 _d2;
};
template<>
class Test<int, char>
{
public:
Test()
{
cout << "调用类模版特化" << endl;
}
private:
int _d1;
char _d2;
};
偏特化(两种):
template<class T1>
class Test<T1, char>
{
public:
Test()
{
cout << "调用偏特化" << endl;
}
private:
int _d1;
char _d2;
};
- 对参数限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
template<class T1, class T2>
class Test<T1*, T2*>
{
public:
Test()
{
cout << "偏特化:参数限制" << endl;
}
private:
T1* _d1;
T2* _d2;
};
了解一下
类模版特化用途之一:类型萃取(仅作了解) 类型萃取了解 C++之类型萃取
模版优缺点
优点 | 缺点 |
---|
模版复用代码,节省资源,方便代码迭代开发 | 模版会导致代码膨胀,也会导致编译时间变长 | 增强了代码的灵活性 | 出现模版编译错误时,错误信息提示不准确,不易定位错误 |
模版还有一个很重要的概念:分离编译!这个概念对于自定义实现模版真的非常重要,详细内容可参考下一篇博客:【C++】模版的分离编译与多文件编程
|