欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++编程之模板与泛型

程序员文章站 2022-06-10 14:37:55
...

模板是一种对类型进行参数化的工具,模板是泛型编程的基础,而泛型编程指的就是编写与类型无关的代码,是C++中一种常见的代码复用方式。模板分为模板函数模板类;模板函数针对参数类型不同的函数;模板类主要针对数据成员和成员函数类型不同的类。

简单的提及了模板的概念,那么模板究竟是怎样实现的呢?我们先举一个模板函数的例子,比如在c语言和c++中使用频率相当之高的swap函数,以前我们写的swap函数通常是针对某种特定类型的,有了模板,我们便可以写出这样的swap函数:

#include<iostream>
using namespace std;
template<class T>
void Swap(T* x,T* y)
{
	T tmp;
	tmp=*x;
	*x=*y;
	*y=tmp;
}
int main()
{
	int a=10;
	int b=20;
	double c=2.0;
	double d=2.4;
	Swap(&a,&b);
	Swap(&c,&d);
	printf("a=%d  b=%d\n",a,b);
	printf("c=%f  d=%f\n",c,d);
	system("pause");
	return 0;
}

用模板实现的这个Swap函数实现了与类型无关的效果,例如:可以交换double类型数据的交换,也可以实现int ,float一些其他类型的交换,很大程度上减少了重复性代码的编写。

注意:模板的声明和定义只能放在全局,命名空间或者类范围内进行,即不能在局部范围或者函数内部进行,比如不能在main函数中声明和定义一个模板。

模板的一般格式为:template<class 形参名1,class 形参名2,...>

                                返回类型  函数名(参数列表){函数体}

其中templateclass是关键字,class可以用typename关键字替代,一般情况下template和class没有什么区别。<>里面的参数叫做模板形参,模板形参和函数形参很像,但是模板形参不能为空,一旦声明了模板函数就可以使用模板函数的形参名声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的地方都可以使用模板形参名。模板形参在调用该模板函数时根据其提供的模板实参类型来初始化模板形参,也就是说一旦编译器确定了实际的模板实参类型就可以称它实例化了函数模板的一个实例。

如对于上述swap函数,我们分别交换了两个int类型的数据和两个double类型的数据,那么在编译器实例化时会分别生成两种类型的代码:

C++编程之模板与泛型

通过反汇编我们可以通过call命令看到两次调用Swap函数调用了两个不同的函数,Swap<int>和Swap<double>;即本质上模板与类型无关是这样实现的;如上述例子中template<class T> void Swap(T* x,T*y){}

当调用这样的模板类型时类型T就会被调用时的类型所替换,比如Swap(&a,&b)中的a和b是int型,这时模板函数Swap中的形参T就会被int所替代,模板函数同时会变成Swap(int* x,int *y),double类型也是如此,这样便实现了函数的编写与类型无关。

对于模板函数而言,不能出现这样的调用Swap(int,int),不能在函数调用的参数中指定模板形参的类型,对函数模板的调用应使用实参的推演来进行。

模板参数的实例化也可以通过显示实例化来进行:

template <typename T>
bool IsEqual (const T& left , const T& right )
{
return left == right;
}
void test1 ()
{
cout<<IsEqual (1,1)<<endl;
//cout<<IsEqual(1,1.2)<<endl; // 模板参数不匹配
cout<<IsEqual<int>(1,1.2)<< endl; // 显示实例化
cout<<IsEqual<double>(1,1.2)<< endl; // 显示实例化
}

模板类

类模板的一般形式为:

template<class 形参1,class 形参2...>
class 类名
{
};

类模板和函数模板一样都是以template开始后接上模板形参列表组成,模板形参不能为空。一旦声明了类模板就可以使用类模板的形参名声明类中的成员变量和成员函数。

例如:

template<class T>
class A
{
public:
A(T a);//构造
A(const A<T>& a);//拷贝构造
A<T>& operator=(const A<T>& a);//赋值运算符的重载
private:
T _a;
};

这里注意区分模板类的类名和类型:其中A<T>为模板类的类型,A为类名,一般来说,在模板类中,只有拷贝构造函数 和构造函数名必须为类名,其他一般均为类型。模板类实现完成之后,模板类对象可以这样创建:类名<类型>  对象名;比如上面的模板类A,则可以创建一个类型为int的模板类对象a(A<int> a;);在类A后面跟上一个<>并在里面填上相应的类型,这样的话凡是用到模板形参的地方都会被int替换,当有两个模板参数时可以这样实例化:类名<类型1,类型2>  对象名;例如:A<int,double> a;类型之间用逗号分隔即可。如需更多的模板类例子可参考下一篇博客(用模板实现顺序表和链表)

非类型模板参数

有时候我们有这样的需要:我们希望可以设计一个指定数组大小的数组模板,有两种方法可以实现我们的需求,第一个是在类中使用动态数组和构造函数参数来提供元素数目;第二个是使用模板参数来提供常规数组的大小。

如:template<class T,size_t n>;其中关键字T为类型参数,size_t指出n的类型为size_t,这种指定为特殊的类型而不用做泛型名称为非类型参数或者表达式参数。

基本用法如下:

//带有非类型模板参数的函数模板
template<class T,int value>
T add(const T& x)
{
	return x+value;
}
//带有非类型模板参数的类模板
template<class T,size_t n>
class Array
{
private:
	int _a;
};
int main()
{
	//将生成两个不同的类模板
	//Array<int,10> a1;
	//Array<int,20> a2; 
	(int(*)(int const&))add<int ,10>;
	cout<<add<int,5>(10)<<endl;
	system("pause");
	return 0;
}

关于非类型模板参数的几点说明:

1.非类型模板参数可以是整型,枚举,引用或者指针,但是不能是浮点数或类对象。

2.模板代码不能修改参数的值,也不能使用参数的地址。

3.实例化模板时,用作非类型参数的值必须是常量表达式。

模板的特化

所谓特化,就是将一些泛型的东西变得具体化,简单来说,就是为已有的模板参数进行一些使其更加特殊化的指定,使得以前不受任何约束的模板的参数受到特定的约束。

针对特化对象的不同,分为两类:函数模板的特化类模板的特化,当函数模板需要对某些类型进行特化处理,称为函数模板的特化。当类模板内需要对某些类型进行特别处理,使用类模板的特化。特化又可以分为全特化和偏特化(半特化)。

概念听起来还是不太清晰,我们通过代码了解一下特化:

全特化就是模板中的模板参数全部被指定为特定的类型,全特化也就是定义了一个新的类型,全特化中的类函数可以与模板类不一样。

template<class T>
int Equal(T x,T y)
{
	return x==y;
}

这是一个简单的判断两个数是不是相等的模板函数,它可以比较两个int类型或者两个double数据以及一些其他类型数据(如string)是否相等,我们把上述模板函数称为原生模板

模板的全特化

全特化模板函数实现后代码如下:

#include<iostream>
#include<string>
using namespace std;
template<class T1,class T2>
bool Equal(T1 x,T2 y)
{
	cout<<"bool Equal(T1 x,T2 y)"<<endl;
	return x==y;
}

template<>
bool Equal(double x,int y)
{
	cout<<"bool Equal(double x,int y)"<<endl;
	return x==y;
}
int main()
{
	cout<<Equal(2.0,1.0)<<endl;//匹配普通函数模板
	cout<<Equal(2.0,1)<<endl;//匹配全特化版本
	cout<<Equal(2,1)<<endl;//匹配普通函数模板
        system("pause");
	return 0;
}
运行结果为:

C++编程之模板与泛型

说明在调用模板函数时会去调用那个匹配它的模板函数。其实全特化就是确定了一个具体类型的函数实现,就是把模板限定死了。

全特化的标志:template<>后面是完全和模板类型完全没有关系的类实现或者函数定义(看起来与普通函数和普通类实现没有区别)。

全特化模板类实现如下:

#include<iostream>
using namespace std;
//原生模板类
template <typename T>
class SeqList
{
public :
SeqList();
~ SeqList();
private :
int _size ;
int _capacity ;
T* _data ;
};
template<typename T>
SeqList <T>:: SeqList()
: _size(0)
, _capacity(10)
, _data(new T[ _capacity])
{
cout<<"SeqList<T>" <<endl;
}
template<typename T>
SeqList <T>::~ SeqList()
{
delete[] _data ;
}
//全特化模板类
template <>
class SeqList <int>
{
public :
SeqList(int capacity);
~ SeqList();
private :
int _size ;
int _capacity ;
int* _data ;
};
// 特化后定义成员函数不再需要模板形参
SeqList <int>:: SeqList(int capacity)
: _size(0)
, _capacity(capacity )
, _data(new int[ _capacity])
{
cout<<"SeqList<int>" <<endl;
}
// 特化后定义成员函数不再需要模板形参
SeqList <int>::~ SeqList()
{
delete[] _data ;
}
void test1 ()
{
SeqList<double > sl2;//匹配原生模板类,因为全特化模板类没有指定为double类型
SeqList<int > sl1(2);//匹配全特化模板类
}
int main()
{
	test1();
	system("pause");
	return 0;
}

运行结果

C++编程之模板与泛型

如果想让SeqList<double> sl1;匹配全特化模板类,则还需要再实现一个doubel类型的全特化模板类。

模板的偏特化

如果模板有多个类型,那么只限定其中一部分类型参数。

假设我们的模板有两个类型参数,接下来分别实现一下偏特化的模板函数和模板类。

偏特化的模板函数:

#include<iostream>
using namespace std;
//原生模板
template<class T1,class T2>
int Add(T1 x,T2 y)
{
	cout<<"int Add(T1 x,T2 y)"<<endl;
	return x+y;
}
//偏特化
template<class T1>
int Add(T1 x,double y)
{
	cout<<"int Add(T1 x,double y)"<<endl;
	return x+y;
}
template<class T2>
int Add(double x,T2 y)
{
	cout<<"int Add(double x,T2 y)"<<endl;
	return x+y;
}
//全特化
template<>
int Add(int x,int y)
{
	cout<<"int Add(int x,int y)"<<endl;
	return x+y;
}
int main()
{
	//cout<<Add(1.0,2.0)<<endl;对重载函数调用不明确
	cout<<Add(1.0,2)<<endl;
	cout<<Add(1,2.0)<<endl;
	cout<<Add(1,2)<<endl;
	system("pause");
	return 0;
}

运行结果为:

C++编程之模板与泛型

可以看出偏特化模板函数在调用时会去调用最匹配的那个函数,其实我感觉函数模板的偏特化就和函数重载差不多,在同一个作用域内函数名相同,函数参数不同,调用时调用与之匹配的那个函数。所以一定程度上可以理解为函数模板不存在偏特化,它的本质就是函数重载,函数模板的偏特化没有存在的必要,直接用函数重载是实现即可。

偏特化的模板类

#include<iostream>
using namespace std;
template <typename T1, typename T2>
class Data
{
public :
        Data();//构造函数
private :
        T1 _d1 ;
        T2 _d2 ;
};
template <typename T1, typename T2>
Data<T1 , T2>:: Data()
{
   cout<<"Data<T1, T2>" <<endl;
}
// 局部特化第二个参数
template <typename T1>
class Data <T1, int>
{
public :
       Data();
private :
       T1 _d1 ;
       int _d2 ;
};
template <typename T1>
Data<T1 , int>:: Data()
{
    cout<<"Data<T1, int>" <<endl;
}
//ps:下面的例子可以看出,偏特化并不仅仅是指特殊部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本。
// 局部特化两个参数为指针类型
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;
}
// 局部特化两个参数为引用
template <typename T1, typename T2>
class Data <T1&, T2&>
{
public :
        Data(const T1& d1, const T2& d2);
private :
        const T1 & _d1;
        const T2 & _d2;
        T1* _d3 ;
        T2* _d4 ;
};
template <typename T1, typename T2>
Data<T1 &, T2&>:: Data(const T1& d1, const T2& d2)
                 : _d1(d1 )
                  , _d2(d2 )
{
   cout<<"Data<T1&, T2&>" <<endl;
}
void test ()
{
    Data<double , int> d1;
    Data<int , double> d2;
    Data<int *, int*> d3;
    Data<int&, int&> d4(1, 2);
}
int main()
{
	test();
	system("pause");
	return 0;
}

运行结果为:

C++编程之模板与泛型

可见偏特化确实总是去调用那个和它最匹配的模板函数,就好比说,你要谈男朋友, 即使你找不到那个各方面全部都适合你的,那么如果有部分条件满足的话是不是也是可以接受的呢?哈哈

针对模板的全特化和偏特化,我接下来要做这样一件事,我将要把原生模板去掉,看一下会发生什么:

C++编程之模板与泛型

编译失败,这个很容易理解,我们假设这样一种情况,我们在一个模板函数或者模板类中,我们如果没写原生模板,只写了全特化和偏特化模板,全特化把类型都限死了,偏特化又把一部分类型限制了,那么如果我们在实例化的过程中,出现了和全特化和偏特化都不匹配的模板类型参数,那么这个代码肯定是编不过的,因为它根本找不到一个匹配自己的模板函数(模板类)。

所以综上:模板的全特化和偏特化都是在已定义的模板基础之上,不能单独存在。即特化必须建立在原生模板的基础上。

类型萃取

类型萃取技术就是要抽取类型的一些具体特征,比如它是哪种具体类型,引用类型,类类型还是内置 类型。类型萃取的技术实际上就是模板技术的具体实现,实现类型萃取基本思想就是:一个是模板的特化,另一个就是typedef来携带一些类型信息。

那为什么会出现类型萃取这种技术呢?

在用模板去实现vector的时候,我们发现这样一个问题,如果我们的实例化参数类型为string时,当在顺序表中插入字符串的时候,超过一定的长度,我们的程序会出现问题,这又是为什么呢?原因就在于string类型里面有一个buf数组,大小为16个字节,当字符串长度大于16个字节时,必然会扩容,当我们进行memcpy(memcpy是内存拷贝)进行内容拷贝的时候,指向原来内存的指针和拷贝完成后的指针指向了同一块内存空间,在对象进行析构的时候这块空间将会被析构两次。这就相当于浅拷贝的问题,程序肯定会崩溃。 那我们应该怎样去解决这个问题,在进行扩容时,我们可以这样做:

void Expand(size_t n)
	{
		if(Empty())
		{
			_start=new T[3];
			_finish=_start;
			_endofstorage=_start+3;
		}
		else if(n>Capacity())
		{
			size_t size=Size();
			T* tmp=new T[n];
			//类型萃取
			for(size_t i=0; i<size; ++i)
			{
				tmp[i]=_start[i];//调用string的赋值运算符
			}
			delete[] _start;
			_start=tmp;
			_finish=_start+size;
			_endofstorage=_start+n;
		}

仔细看,如果是string类型模板,我们用一个for循环里面采用调用string内部赋值运算符的方法来解决了这个问题,而我们库里的string类在赋值运算符实现中已经解决了这个问题。关于string类的赋值运算符重载我这里不说明。有兴趣可以去了解一下库里的string类的实现。

类型萃取的方式:类型萃取是在模板的基础上区分内置类型和自定义类型,主要原理是将内置类型全部特化,然后进行区分。来看下面这段代码:

#include<iostream>
using namespace std;
#include<string>
struct __TrueType
{};

struct __FalseType
{};

template<class T>
struct TypeTraits
{
	typedef __FalseType IsPodType;
};

template<>
struct TypeTraits<int>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<char>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<unsigned char>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<double>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<long double>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<float>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<unsigned int>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<long>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<unsigned long>
{
	typedef __TrueType IsPodType;
};

template<>
struct TypeTraits<long long>
{
	typedef __TrueType IsPodType;
};

template<class T>
T*  __TypeCopy(const T* src,T* dst,size_t n,__TrueType t)
{
	cout<<"memcpy:"<<typeid(T).name()<<endl;
	return (T*)memcpy(dst,src,sizeof(T)*n);
}

template<class T>
T*  __TypeCopy(const T* src,T* dst,size_t n,__FalseType t)
{
	cout<<"for+operate:"<<typeid(T).name()<<endl;
	for(size_t i=0; i<n; ++i)
	{
		dst[i]=src[i];
	}
	return dst;

}

template<class T>
T*  TypeCopy(const T* src,T* dst,size_t n)
{
	return __TypeCopy(src,dst,n,TypeTraits<T>::IsPodType());
}
void TestTypeCopy()
{
	string s1[3]={"11","22","33"};
	string s2[3]={"00"};
	int a1[3]={1,2,3};
	int a2[3]={0};
        TypeCopy(s1,s2,2);
        TypeCopy(a2,a2,3);
}

运行结果为:

C++编程之模板与泛型

可以看出根据不同的类型(内置类型和自定义类型)分别调用了不同的方法。

关于上述函数有以下几点说明:

1.在C++中我们常用typeid(T).name()函数来提取类型的名称。(可以看出string的类型还是挺长的)

2.POD(plain old type):平凡类型,就是指基本类型(如int,float,double之类的)

那上述代码示怎样实现类型萃取的呢?也就是上述类型的过程是怎么走的呢?

来看图:

C++编程之模板与泛型

int类型与此类似。

 模板的分离编译

首先我们先上一段代码:

//template.h
#pragma once
#include<iostream>
using namespace std;
void func();
template<class T>
void TFunc();

//template.cpp
#include "template.h"
void func()
{
	cout<<"一般函数"<<endl;
}
template<class T>
void TFunc()
{
	T t;
	cout<<"模板函数"<<endl;
}

//test.cpp
#include "template.h"
int main()
{
	func();
	TFunc<int>();
	system("pause");
	return 0;
}

在我们平时写代码的时候,一般会将函数的声明放在.h里面,将函数的定义放在.cpp里面。经过编译我们发现普通函数(func)运行并没有问题,但是模板函数链接期间却出现了这样一个错误:

C++编程之模板与泛型

这是为什么呢?首先对于我们的普通函数func而言,它在调用的时候在template.cpp中找到了func函数的地址,但是我们的Tfunc函数却没有找到TFunc的地址,我们的模板函数只有实例化后才会生成具体的函数,而在本例中并没有进行实例化。所以编译器过后在底层的符号列表里有func函数的地址,却没有TFunc函数的地址。所以链接会出错。

那怎么解决这个问题呢?

有两种方案:

1. 在模板头文件.h 里面显式实例化

如下述代码:

#pragma once
#include<iostream>
using namespace std;
void func();
template<class T>
template
void TFunc<int>();

一般不推荐这种方法,一方面老编译器可能不支持,另一方面实例化依赖调用者。(不推荐)

2. 将声明和定义放到一个文件"xxx.hpp"(.cpp和.h的结合) 里面(推荐使用这种方法)。

#pragma once
#include<iostream>
using namespace std;
void func();
template<class T>
void TFunc()
{
cout<<"模板函数"<<endl;
}

关于模板的总结:

优点:

1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。
2. 增强了代码的灵活性。
缺点:
1. 模板让代码变得凌乱复杂,不易维护,编译代码时间变长。
2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误