C++用模板来实现泛型编程,模板分为函数模板和类模板。
基本概念:泛型编程范式GP:模板也叫参数类型多态化。泛型在编译时期确定,相比面向对象的虚函数多态,能够有更高的效率。
泛型编程是从一个抽象层面描述一种类型的算法,不管容器类型是什么,是一种不同于OOP的角度来抽象具体算法。
C++0X目前对GP的支持的趋势来看,确实如此,auto/varadic templates这些特性的加入象征着C++ GP的形式正越来越转向一种更纯粹的泛性语法描述。
GP的一个主要的弱点就是它不是二进制可复用的,源码基本是公开的,因为编译时候才决定具体类型,决定了类型后就不能更改了不像OOP一样拥有关闭开放的原则。
本文主要对函数模板和类模板,在定义语法,模板内部使用泛型类型和非泛型类型,模板的声明和定义分离实现方法,外部客户调用泛型和非泛型参数类型的模板方法,这四个方面比较系统的总结了C++中泛型的使用经验,博客前面的文字有些凌乱建议想了解本文讲述的泛型使用经验的读者把代码拷贝下来,运行一遍分析代码的用意。
本文主要对函数模板和类模板,在定义语法,模板内部使用泛型类型和非泛型类型,模板的声明和定义分离实现方法,外部客户调用泛型和非泛型参数类型的模板方法,这四个方面比较系统的总结了C++中泛型的使用经验,博客前面的文字有些凌乱建议想了解本文讲述的泛型使用经验的读者把代码拷贝下来,运行一遍分析代码的用意。
一、函数模板
1.函数模板的定义
1.1 模板的定义,用template和typename声明后,函数内直接使用;函数不支持template参数列表是空的,类重复具体定义时才支持。
1.2. 非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
关于形参:如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。
2.函数模板内部使用泛型和非泛型类型
泛型类型定义后,函数内部直接使用即可,类型变量符合作用域规则,包括不可重定义或覆盖,可见作用域。非泛型类型直接使用。
3.函数模板声明和实现分离原因和做法
将Template函数的声明和实现分离,声明在.h中,实现在.cpp中,其中.h需要包含.cpp;类模板中不能这样做,需要特殊处理。
需要分离的原因有:
真正原因:
1.简化条理化代码,实现声明和实现分离,高内聚低耦合,对外提供简单的接口,对内高内聚 。
1.简化条理化代码,实现声明和实现分离,高内聚低耦合,对外提供简单的接口,对内高内聚 。
几乎不会出现,STL都没有分离:
2.将模板类的声明与实现都放在.h中(在多个cpp中使用不同模板参数时可能会引起重复定义的编译错误)。
2.将模板类的声明与实现都放在.h中(在多个cpp中使用不同模板参数时可能会引起重复定义的编译错误)。
4.客户对函数模板的使用
4.1 实参推演和类型匹配
实参推演是:模板函数不支持类型显式实例化声明的,直接用实参变量实例化调用就好。
类型匹配是:一种类型的只能是一种类型,不能两种类型传入一种类型的模板函数中,是类型引用别名的不能用实例值传入。
4.2 非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
二、类模板
1.类模板的定义
1.1.定义类模板语法
例如:
template<class 形参名,class 形参名,…> class 类名
1.2.非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
关于形参:如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。
1.3 针对类模板声明附加特性,模板子类继承半具体化的模板父类,以及子类调用父类函数:
template< typename T, int N >
class TinyVector
{
public:
TinyVector();
TinyVector( const T& value );
template< typename T2 >
TinyVector( const TinyVector<T2,N>& v );
...
}
template< typename T >
class Point3D : public TinyVector<T,3>
{
public:
Point3D() {}
Point3D( T x, T y, T z )
{
(*this)(0) = x;
(*this)(1) = y;
(*this)(2) = z;
}
Point3D( const TinyVector<T,3>& v )
: TinyVector<T,3>(v)
{
}
const T& x() const { return this->at(0); }
const T& y() const { return this->at(1); }
const T& z() const { return this->at(2); }
T& x() { return (*this)(0); }
T& y() { return (*this)(1); }
T& z() { return (*this)(2); }
};
2)模板子类继承半具体化的模板父类
1).模板类的继承,继承还是和正常类一样的
例如:template< typename T, int N >
class TinyVector
{
public:
TinyVector();
TinyVector( const T& value );
template< typename T2 >
TinyVector( const TinyVector<T2,N>& v );
...
}
template< typename T >
class Point3D : public TinyVector<T,3>
{
public:
Point3D() {}
Point3D( T x, T y, T z )
{
(*this)(0) = x;
(*this)(1) = y;
(*this)(2) = z;
}
Point3D( const TinyVector<T,3>& v )
: TinyVector<T,3>(v)
{
}
const T& x() const { return this->at(0); }
const T& y() const { return this->at(1); }
const T& z() const { return this->at(2); }
T& x() { return (*this)(0); }
T& y() { return (*this)(1); }
T& z() { return (*this)(2); }
};
2)模板子类继承半具体化的模板父类
模板不能是空模板参数类型,但是声明了正常的声明了一个模板类后,如果要对这个模板类进行更加具体的限定那么可以重新定义这个模板的细节,这个时候模板参数类型可以为空,但是这个时候重定义的名称一定要相同,使用的模板参数也是要定义了的。
例如template<> class BinaryNumericTraits1会报错,template<> class BinaryNumericTraits<T1, double>也会报错,正确定义如下:
template< typename T1, typename T2 >
class BinaryNumericTraits
{
public:
typedef T1 OpResult;
};
template<>
class BinaryNumericTraits<int, double>
{
public:
typedef double OpResult;
};
template<>
class BinaryNumericTraits<double, int>
{
public:
typedef double OpResult;
};
3)子类调用父类函数方式
3)子类调用父类函数方式
// 继承中调用父类的方法
Array<T,2>::operator= ( Array<T,2>(value_) );
template <typename T, int N>
class Array
{
public:
Array()
{
for(int i = 0; i < N; i++)
{
data[i] = 0;
}
}
Array(T t1)
{
for(int i = 0; i < N; i++)
{
data[i] = t1;
}
}
int GetSize() {return N;}
T GetValue(int nIndex ){ return data[nIndex];}
private:
T data[N];
};
#include <iostream>
using namespace std;
template <typename T>
class Array2D: public Array<T,2>
{
public:
Array2D( int m, int n );
void Display()
{
cout<<"Array2D Value:";
for(int i = 0; i < GetSize(); i++)
{
cout<<GetValue(i)<<",";
}
cout<<endl;
}
};
template <typename T>
Array2D<T>::Array2D(int m, int n): Array<T,2>()
{
int value_ = m * n;
// 这个语法其实和 Array<T,2>(value_) 是一样的
Array<T,2>::operator= ( Array<T,2>(value_) );
}
调用端:
Array2D<int> arrayObj(1,2);
arrayObj.Display();
2.类模板内部使用泛型和非泛型类型,类外定义函数成员
2.1使用类模板的类型声明,包括数据成员,和函数参数、函数返回值都可以直接使用泛型类型。类型变量符合作用域规则,包括不可重定义或覆盖,可见作用域。
非泛型类型直接使用。
非泛型类型直接使用。
2.2 在类声明外定义类函数(在CPP中也是可以的,但是包含情况就会发生变化),格式如下:
template<模板形参列表> 函数返回类型 类名<模板形参名>::函数名(参数列表){函数体}
// 例如
template< typename T, int N >
class TinyVector
{
public:
// 模板类里面的正常函数
TinyVector( const T& value );
// 模板类里面含有模板函数
template< typename T2 >
TinyVector<T,N>& operator= ( const TinyVector<T2,N>& v );
}
外部实现:
// 模板类的正常函数
template< typename T, int N >
TinyVector<T,N>::TinyVector( const T& value )
{
for ( int i = 0; i < N; ++i ) {
data_[i] = value;
}
}
// 模板类里面的模板函数
template< typename T, int N >
template< typename T2 >
TinyVector<T,N>& TinyVector<T,N>::operator= ( const TinyVector<T2,N>& v )
{
for ( int i = 0; i < N; ++i ) {
data_[i] = v(i);
}
return *this;
}
// 例如
template< typename T, int N >
class TinyVector
{
public:
// 模板类里面的正常函数
TinyVector( const T& value );
// 模板类里面含有模板函数
template< typename T2 >
TinyVector<T,N>& operator= ( const TinyVector<T2,N>& v );
}
外部实现:
// 模板类的正常函数
template< typename T, int N >
TinyVector<T,N>::TinyVector( const T& value )
{
for ( int i = 0; i < N; ++i ) {
data_[i] = value;
}
}
// 模板类里面的模板函数
template< typename T, int N >
template< typename T2 >
TinyVector<T,N>& TinyVector<T,N>::operator= ( const TinyVector<T2,N>& v )
{
for ( int i = 0; i < N; ++i ) {
data_[i] = v(i);
}
return *this;
}
3.关于模板声明和实现分离后的调用原理
模板不能分离的原因:C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,cpp中没有将模板类实例化,所以实际上.cpp编译出来的t.obj文件中关于模板实例类型的一行二进制代码,于是连接器就傻眼了,只好给出一个连接错误。所以这里将.cpp包含进来,其实还是保持模板的完整抽象,main中可以对完整抽象的模板实例化。
进行分离的方法:
类的声明必须在实现的前面,所以使用的时候包含cpp就可以把.h和.cpp中定义的内容一并包含了,其实是放在同一个文件中一个意思。
这种模板分离的方式缺点:如果这个模板类的cpp有非模板的定义,能够有效实例化,但是会导致重复包含定义而出错所以有些模板的实现和分离放到了.tcc格式或者.inl格式的文件中(这些格式的文件都是来自于txt文本的不能从cpp直接修改得到),在.h中直接包含进去,代码中就可以直接包含ClassTemplate.h了,避免包含.cpp奇怪的行为。
例如:
#include "Test.tcc"
4. 客户对类模板的使用
4.1 模板类的使用,类对象显式类型声明不支持实参推演,类的成员函数要求实参推演和类型匹配。
4.2 非泛型类型形参,一般是简单类型,非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量。
三、关于函数模板和类模板的定义、定义使用、声明和定义分离,客户使用的规则的测试代码
上面的总结代码中都用,测试工程中包含6个文件,分别为:
FunctionTempplate.h
FunctionTempplate.cpp
ClassTemplate.h
ClassTemplate.tcc
mian.cpp
main2.cpp(测试特性用的)
FunctionTempplate.cpp
ClassTemplate.h
ClassTemplate.tcc
mian.cpp
main2.cpp(测试特性用的)
FunctionTempplate.h
/*0.基本概念:泛型编程范式GP:模板也叫参数类型多态化。
在编译时期确定,相比面向对象的虚函数多态,能够有更高的效率。
泛型编程是从一个抽象层面描述一种类型的算法,不管容器类型是什么,是一种不同于OOP的角度来抽象具体算法。
C++0X目前对GP的支持的趋势来看,确实如此,auto/varadic templates这些特性的加入象征着C++ GP的形式正越来越转
向一种更纯粹的泛性语法描述。
GP的一个主要的弱点就是它不是二进制可复用的,源码基本是公开的,因为编译时候才决定具体类型,决定了类型后就不能更改了不像OOP一样拥有关闭开放的原则。
*/
#ifndef FUNCTIONTEMPPLATE_H
#define FUNCTIONTEMPPLATE_H
// 1.1 模板的定义和实现,template和typename声明后,函数内直接使用;函数不支持template参数列表是空的,类才支持
#include "FunctionTempplate.cpp"
template<typename T1,typename T2>
void SwapTwoType(T1 &t1, T2 &t2)
{
T1 temp = t1;
t1 = (T1)t2;
t2 = (T2)temp;
}
// 1.2. 非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
// 关于形参:如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。
template<typename T>
bool TestOver(T t1, int t2)
{
return int(t1) > t2;
}
// 2.泛型类型定义后,函数内部直接使用即可,类型变量符合作用域规则,包括不可重定义或覆盖,可见作用域
template<typename T>
void SwapOneType(T &t1, T &t2)
{
T temp = t1;
t1 = t2;
t2 = temp;
}
// 函数不支持template是空的,类才支持,也不支持SwapImp<double &t1, int &t2>这样的写法
//template<>
//void SwapImp<double &t1, int &t2>
//{
// double temp = t1;
// t1 = t2;
// t2 = (int)temp;
//};
// 3.函数模板声明和实现分离原因和做法
// 将Template函数的声明和实现分离,声明在.h中,实现在.cpp中,其中.h需要包含.cpp
// 需要分离的原因有:
//真正原因:1.简化条理化代码,实现声明和实现分离,高内聚低耦合,对外提供简单的接口,对内高内聚
//几乎不会出现,STL都没有分离:2.将模板类的声明与实现都放在.h中(在多个cpp中使用不同模板参数时可能会引起重复定义的编译错误)
template<typename T>
void SwapOneTypeImp(T &t1, T &t2);
#endif
FunctionTempplate.cpp
template<typename T>
void SwapOneTypeImp(T &t1, T &t2)
{
T temp = t1;
t1 = t2;
t2 = temp;
}
ClassTemplate.h
#ifndef CLASSTEMPLATE_H
#define CLASSTEMPLATE_H
// 1.1.声明类模板,例如:
// template<class 形参名,class 形参名,…> class 类名
template<typename T>
class CMathOperation
{
// 2.1使用类模板的类型声明,包括数据成员,和函数参数、函数返回值都可以直接使用泛型类型
// 类型变量符合作用域规则,包括不可重定义或覆盖,可见作用域
public:
T Add(T &t1, T &t2)
{
return t1 + t2;
}
T Multi(T &t1, T &t2);
T Min(T &t1, T &t2);
// 1.2.非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
// 关于形参:如果是类型形参,我们就知道该形参表示未知类型,如果是非类型形参,我们就知道它是一个未知值。
bool bOverMax(T &t1, int maxNum);
private:
T a;
T b;
};
// 2.2 在类声明外定义类函数(在CPP中也是可以的,但是包含情况就会发生变化),格式如下:
// template<模板形参列表> 函数返回类型 类名<模板形参名>::函数名(参数列表){函数体}
template<typename T> T CMathOperation<T>::Multi(T &t1, T &t2)
{
return t1*t2;
}
// 3.这种模板分离的方式缺点:如果这个模板类的cpp有非模板的定义,能够有效实例化,那么会导致重复包含定义而出错
// 所以有些模板的实现和分离放到了.tcc格式或者.inl格式的文件中(这些格式的文件都是来自于txt文本的不能从cpp直接修改得到),
// 在.h中直接包含进去,代码中就可以直接包含ClassTemplate.h了,避免包含.cpp奇怪的行为;这种方式编辑时候需要修改下后缀。
#include "ClassTemplate.tcc"
// 1.3 针对类模板声明附加特性:
// 模板不能是空模板参数类型,但是声明了正常的声明了一个模板类后,如果要对这个模板类进行更加具体的限定那么可以
// 重新定义这个模板的细节,这个时候模板参数类型可以为空,但是这个时候重定义的名称一定要相同,使用的模板参数也是要定义了的。
// 例如template<> class BinaryNumericTraits1会报错,template<> class BinaryNumericTraits<T1, double>也会报错,正确定义如下:
template< typename T1, typename T2 >
class BinaryNumericTraits
{
public:
typedef T1 OpResult;
};
template<>
class BinaryNumericTraits<int, double>
{
public:
typedef double OpResult;
};
template<>
class BinaryNumericTraits<double, int>
{
public:
typedef double OpResult;
};
#endif
ClassTemplate.tcc
// 类模板可以直接在.h中声明,在文本文件中定义
template<typename T>
T CMathOperation<T>::Min(T &t1, T &t2)
{
return t1 > t2 ? t2 : t1;
}
template<typename T>
bool CMathOperation<T>::bOverMax( T &t1, int maxNum )
{
if( t1 > maxNum )
{
return true;
}
return false;
}
// 如果这个模板类的cpp有非模板的定义,能够有效实例化,那么会导致重复包含定义而出错
// 所以有些模板的实现和分离放到了.tcc格式或者.inl格式的文件中,在.h中直接包含进去
//int g_TestValue = 100;
//bool IsNegtiveValue()
//{
// if( g_TestValue < 0 )
// {
// return true;
// }
// return false;
//}
mian.cpp
#include "FunctionTempplate.h"
// 3.关于模板声明和实现分离后的调用原理
// 类的声明必须在实现的前面,所以使用的时候包含cpp就可以把.h和.cpp中定义的内容一并包含了,其实是放在同一个文件中一个意思
// 原因是:C++标准明确表示,当一个模板不被用到的时侯它就不该被实例化出来,
// cpp中没有将模板类实例化,所以实际上.cpp编译出来的t.obj文件中关于模板实例类型的一行二进制代码,
// 于是连接器就傻眼了,只好给出一个连接错误。
// 所以这里将.cpp包含进来,其实还是保持模板的完整抽象,main中可以对完整抽象的模板实例化。
// 这种模板分离的方式缺点:如果这个模板类的cpp有非模板的定义,能够有效实例化,那么会导致重复包含定义而出错
// 所以有些模板的实现和分离放到了.tcc格式或者.inl格式的文件中(这些格式的文件都是来自于txt文本的不能从cpp直接修改得到),
// 在.h中直接包含进去,代码中就可以直接包含ClassTemplate.h了,避免包含.cpp奇怪的行为。
#include "ClassTemplate.h"
#include <iostream>
using namespace std;
int n1,n2;
double dValue1, dValue2;
void ResetTestData()
{
n1 = 10;
n2 = 100;
dValue1 = 1.5f;
dValue2 = 16.5f;
}
void DisplayTestData(int nCount)
{
cout<<"---------------Result of count: "<<nCount<<"-------------"<<endl;
cout<<"n1= "<<n1<<",n2= "<<n2<<",dValue1= "<<dValue1<<",dValue2= "<<dValue2<<endl;
}
extern void TestSwapFunc();
void main()
{
// 一、函数模板使用
cout<<"-------------函数模板使用------------"<<endl;
TestSwapFunc();
ResetTestData();
// 4.1 函数模板的使用,实参推演和类型匹配
// 实参推演是:模板函数不支持类型显式实例化声明的,直接用实参变量实例化调用就好。
// 类型匹配是:一种类型的只能是一种类型,不能两种类型传入一种类型的模板函数中,是类型引用别名的不能用实例值传入。
//SwapOneType(<int>(n1), <int>(n2));语法错误 : 缺少“)”(在“<”的前面),Swap(T1 &,T2 &)”: 应输入 2 个参数,却提供了 0 个
//SwapTwoType(<int>(n1), <int>(n2)); 语法错误 : 缺少“)”(在“<”的前面),Swap(T1 &,T2 &)”: 应输入 2 个参数,却提供了 0 个
//SwapOneType(2, 3);不能将参数 1 从“int”转换为“int &”
//SwapTwoType(2, 3);不能将参数 1 从“int”转换为“int &”
SwapTwoType(n1, n2); // OK
DisplayTestData(1);
//SwapTwoType(2, 3); // 错误,SwapTwoType”: 不能将参数 1 从“int”转换为“int &”
ResetTestData();
SwapTwoType(dValue1, dValue2); // OK,直接实例化
DisplayTestData(2);
ResetTestData();
SwapTwoType(n1, dValue1);// 也OK,直接实例调用因为是支持两个类型的,只是dValue1的1.5f转换为n1时候被截取了整型变为了1
DisplayTestData(3);
ResetTestData();
//SwapOneType(n1, dValue1);// 编译错误,因为只有一个类型,void SwapOneType(T &,T &)”: 模板 参数“T”不明确
SwapOneType(dValue1, dValue2); // OK,同样的一个类型实例化
DisplayTestData(4);
ResetTestData();
SwapOneTypeImp(dValue1, dValue2);
DisplayTestData(5);
// 4.2 非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
int nCurNum = 10;
int nMaxValue = 100;
bool bTestOver = TestOver(nCurNum, nMaxValue);
// 二、类模板使用
// 4.1 模板类的使用,类对象显式类型声明不支持实参推演,类的成员函数要求实参推演和类型匹配
cout<<"-------------类模板使用------------"<<endl;
CMathOperation<int> oprObj; // OK,必须要显式类型指定,不能用函数模板的实参推演
int a = 10;
int b = 15;
int nAddResult = oprObj.Add(a,b); // OK,类成员函数参数不能使用显式类型指定了
int nMultiResult = oprObj.Multi(a,b);// OK,类外部定义也是可以的
//int nAddResult2 = add.Add(<int>(a),<int>(b));// NO 显式类型指定是错误的
//float fTestValue = 15.0f;
//int nAddResult = add.Add(a, fTestValue);// NO 和函数模板一样也是要求类型匹配的
/*float fValue1 = 1.5f;
float fValue2 = 3.5f;
float fRes3 = oprObj.Add(fValue1, fValue2);*/ // NO 对象是int类型的,因为类型匹配,故其它类型的参数传入报错
int minNum = oprObj.Min(a,b);
// 4.2 非泛型类型形参,只能是简单类型
int nValue = 10;
bool bOver = oprObj.bOverMax(nValue, 100); // OK
int nMaxValue1 = 100;
bool bOver1 = oprObj.bOverMax(nValue, nMaxValue1); // 也是OK的,只要类型匹配就可以了
// 非泛型类型参数,直接使用就可以了,可以传入该类型的常量或者变量
const int nMaxValue2 = 100;
bool bOver2 = oprObj.bOverMax(nValue, nMaxValue2); // 也是OK的,只要类型匹配就可以了
while(1);
}
main2.cpp(测试特性用的)
//本文件主要针对于不同的模板传入不同的参数会导致编译重定义问题
// 几乎不会出现,STL都没有分离:
// 将模板类的声明与实现都放在.h中(在多个cpp中使用不同模板参数时可能会引起重复定义的编译错误)
#include "FunctionTempplate.h"
// 将模板cpp多次包含是不会参数多重包含的,因为编译时候这个模板的cpp并不能有效的实例化。
//#include "ClassTemplate.tcc"
void TestSwapFunc()
{
float fTestValue1 = 1.0f;
float fTestValue2 = 1000.0f;
double dValue3 = 10.001f;
SwapOneType(fTestValue1, fTestValue2);
SwapTwoType(fTestValue1, dValue3);
}