泛型编程

泛型编程是编写和类型无关的逻辑代码,是代码复用的另一种手段。模板是实现泛型编程的一种技术。

以往要编写一个通用的函数有3种方法,函数重载,使用公共基类,宏函数。

然而函数重载需要对各种类型一一枚举,且一个错个个错。第二种继承公共基类并重写虚函数实现多态,失去了类型检查,对每一个子类维护也很麻烦。宏函数也没有类型检查。

实现Vector和List容器https://github.com/zzaiyuyu/Vector-List

模板技术

模板实例化

不管是类模板或是函数模板,都不是真正的类或函数,只是一个编译器用来生成代码的蓝图

模板的实例化指函数模板(类模板)生成模板函数(模板类)的过程。对于函数模板而言,模板实例化之后,会生成一个真正的函数。而类模板经过实例化之后,只是完成了类的定义,模板类的成员函数需要到调用时才会被初始化。模板的实例化分为隐式实例化和显示实例化。

隐式实例化:在发生模板函数调用或者模板类时,才进行实例化。

//隐式实例化函数模板
template <typename T> 
T Max(const T& t1,const T& t2){
    return (t1>t2)?t1:t2;
}
//隐式实例化类模板
template <typename T>
class A{
    T num;
}

显示实例化:不管有无调用,只要显式声明,必定实例化出模板类或函数

//用template声明一下函数模板
template void func<int>(const int&);
//类模板
template class theclass<int>;

模板的使用

对函数模板的使用而言,分为两种调用方式,一种是显示模板实参调用(显示调用),一种是隐式模板实参调用(隐式调用)。对于类模板的使用而言,没有隐式模板实参和显式模板实参使用的说法,因为类模板的使用必须显示指明模板实参

  1. 隐式调用,编译器根据调用的实参推演出匹配的模板函数。
  2. 显示调用,由用户决定调用的实参类型,不经过参数推演
template <typename T> 
T Max(const T& t1,const T& t2){
    return (t1>t2)?t1:t2;
}

int main(){
    int i=5;
    cout<<Max(i,'a')<<endl; //隐式调用,没有匹配的参数无法通过编译
    cout<<Max<int>(i,'a')<<endl; //显示调用,强转int,通过编译
}

模板参数

template<typename T, size_t N>      //模板参数,其中T是类型参数,N是非类型参数
void PrintArray(const T (&array)[N]) { 
    for(size_t i = 0; i < N; ++i)     
    cout<<array[i]<<" "; 
    cout<<endl; 
}

int arr[10];
PrintArray(arr);    //调用参数

<>里typename后跟类型参数,其他为非类型参数。非类型参数N会根据实参决定,作为常量在函数模板里使用。

模板参数的作用域在紧跟的函数模板里有效。

模板特化

函数模板有全特化,类模板有全特化和偏特化。

有时候在一个泛型模板里并不能正确处理所有的类型,所以需要特化。

函数模板特化

//函数模板
template<class T>
bool IsEqual(T t1,T t2){
    return t1==t2;
}

template<> //函数模板特化
bool IsEqual(char *t1,char *t2){
    return strcmp(t1,t2)==0;
}

另外函数重载也可以实现函数模板特化的功能

bool IsEqual(char *t1,char *t2){
    return strcmp(t1,t2)==0;
}

如果使用普通重载函数,那么不管是否发生实际的函数调用,都会在目标文件中生成该函数的二进制代码。分离编译模式下,应该在各个源文件中包含重载函数的声明重载的函数。

类模板特化

全特化:模板中参数全部被固定,相当于定义一个全新的类

偏特化:模板参数没有全部固定,对部分模板参数进行指定限制

类模板全特化的应用场景——类型萃取

struct TrueType {
    static bool Get()
    {
        return true;
    }
};

struct FalseType {
    static bool Get()
    {
        return false;
    }
};
//如果没有特化,就是自定义类型
template<class T>
struct TypeTraits {
    typedef FalseType PODTYPE;
};
template <>
struct TypeTraits<int> {
    typedef TrueType PODTYPE;
};
template <>
struct TypeTraits<double> {
    typedef TrueType PODTYPE;
};
//调用
//TypeTraits<T>::PODTYPE::Get()

偏特化不仅是对部分参数的特化,同时可以是对参数的限制

常见的有将参数特化为绝对类型,指针类型

// 局部特化两个参数为指针类型
template <typename T1, typename T2> 
class Data <T1*, T2*> {
public :    
    Data(); 
private :   
    T1 _d1 ;   
    T2 _d2 ;   
    T1* _d3 ;  
    T2* _d4 ;   
}; 
template <typename T1, typename T2> 
    Data<T1 *, T2*>:: Data() { 
    cout<<"Data<T1*, T2*>" <<endl; 
}

容器适配器

已有的类模板比如vector,list,deque对操纵数据的封装,称为容器。进一步想实现一些数据结构可以借助容器的接口快速实现。

template<class T, class Container = SeqList<T> > 
class{
    ...
private:
    Container _con;
}

分离编译模式

https://blog.csdn.net/pongba/article/details/19130

上文总结一下:

每个cpp文件都会先把包含的.h复制过来,然后编译成一个单独的obj目标文件。正常情况下一个cpp里找不到其他cpp文件里的函数,会在链接期间从其他obj文件的导出符号表里找到需要的函数,并把函数地址填到自己的调用函数处。

由于模板的特殊性,只有在同一个cpp文件里发现有调用才会实例化出真正的模板函数,当函数模板单独放在一个cpp里,编译器就不会为其实例化,在链接期间别人也不可能找得到没有实例化的函数。

解决方案:

  1. 在模板头文件 xxx.h 里面显示实例化
  2. 将声明和定义放到一个文件 “xxx.hpp” 里面

hpp实质就是将.cpp的实现代码混入.h头文件当中,定义与实现都包含在同一文件,则该类的调用者只需要include该hpp文件即可.**实现代码将直接编译到调用者的**obj文件中,不再生成单独的obj

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐