模板进阶
1. 非类型模板参数
模板参数分为类型形参和非类型形参
类型形参:出现在模板参数列表中,跟再class或者typename之后的参数类型名称
非类型形参:就是用一个常量作为类(函数)模板的一个参数,在类(函数)模板中可将该参数当成常量来使用
【注意】:
浮点数,类对象和字符串是不允许作为非类型模板参数的
非类型模板参数必须在编译期就能确定结果
例:使用非类型模板参数定义一个模板类型的静态数组
// 定义一个模板类型的静态数组 template<class T, size_t N = 10> class Array { public: T& operator()(size_t index) { return _array[index]; } const T& operator[](size_t index)const { return _array[index]; } size_t Size()const { return _size; } bool Empty()const { return 0 == _size; } private: T _array[N]; size_t _size; };
2. 模板的特化
2.1 概念
通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结果
例:
template <class T> bool IsEqual(T& left, T& right) { return left == right; } void Test() { char* p1 = "hello"; char* p2 = "world"; if (IsEqual(p1, p2)) { std::cout << p1 << std::endl; } else { cout << p2 << endl; } }
此时就需要对模板进行特化,即:在原模板类的基础上,针对特殊类型所进行特殊化的实现方式。模板特化中分为函数模板特化和类模板特化
2.2 函数模板特化
函数模板特化的步骤:
必须要先有一个基础的函数模板
关键字template后面跟一对空的尖括号<>
函数名后面跟一对尖括号<>,尖括号中指定需要特化的类型
函数形参表:必须要和模板函数的基础参数类型完全相同,如果不同编译器可能汇报一些奇怪的错误
template <> bool IsEqual<char*>(char*& left, char*& right) { if (strcmp(left, right) > 0) { return true; } return false; }
【注意】:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了简单实现通常都是将该函数直接给出
bool IsEqual(char* left, char* right) { if (strcmp(left, right) == 0) { return true; } return false; }
2.3 类模板特化
2.3.1 全特化
全特化就是将模板参数列表中的所有参数都确定化
template<class T1, class T2> class Data { public: Data() {cout<<"Data<T1, T2>" <<endl;} private: T1 _d1; T2 _d2; }; template<> class Data<int, char> { public: Data() {cout<<"Data<int, char>" <<endl;} private: T1 _d1; T2 _d2; }
2.3.2 偏特化
偏特化:任何针对模板参数进行条件限制设计的特化版本
template<class T1, class T2> class Data { public: Data() {cout<<"Data<T1, T2>" <<endl;} private: T1 _d1; T2 _d2; };
偏特化有以下两种表现形式:
部分特化
将模板参数列表中的一部分参数特化
// 将第二个参数特化为int template <class T1> class Data<T1, int> { public: Data() {cout<<"Data<T1, int>" <<endl;} private: T1 _d1; int _d2; };
参数更近一步限制
偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
//两个指针偏特化为指针类型 template <calss T1, class T2> class Data<T1*, T2*> { public: Data() {cout <<"Data<T1*, T2*>"<<endl;} private: T1 _d1; T2 _d2; } //两个参数偏特化为引用类型 template<calss T1, class T2> class Data<T1&, T2&> { public: Data(const T1& d1, const T2& d2) : _d1(d1) , _d2(d2) { cout<<"Data<T1&, T2&>"<<endl; } } private: const T1& _d1; const T2& _d2; }; void test2 () { Data<double , int> d1; // 调用特化的int版本 Data<int , double> d2; // 调用基础的模板 Data<int *, int*> d3; // 调用特化的指针版本 Data<int&, int&> d4(1, 2); // 调用特化的指针版本 }
3. 类模板特化应用之类型萃取
如何实现一个通用的拷贝函数?
3.1 使用memcpy拷贝
template <class T> void Copy(T* dst, const T* src, size_t size) { memcpy(dst, src, szieof(T)*szie); }
上述代码虽然对于任意类型的空间都可以进行拷贝,但是如果拷贝自定义类型的对象就会出错,因为自定义类型对象有可能涉及到深拷贝(比如:string),而memcpy属于浅拷贝。如果对象中涉及到资源管理,就只能用赋值
3.2 使用赋值方式拷贝
template <class T> void Copy(T* dst, const T* src, size_t size) { for (size_t i = 0; i < size; ++i) { dst[i] = src[i]; } }
用循环赋值的方式可以,但是代码的效率比较低下,而C/C++程序最大的优势就是效率高。那能否将另种方式的优势结合起来呢?遇到内置类型就用memcpy来拷贝,遇到自定义类型就用循环赋值方式来做呢?
3.3 增加bool类型区分自定义类型与内置类型
template<class T> void Copy(T* dst, const T* src, size_t size, bool IsPODType) { if(IsPODType) memcpy(dst, src, sizeof(T)*size); else { for(size_t i = 0; i < size; ++i) dst[i] = src[i]; } }
通过多增加一个参数,就可将两种拷贝的优势体现结合起来。但缺陷是:用户需要根据所拷贝元素的类型去传递第三个参数,那出错的可能性就增加。那能否让函数自动去识别所拷贝类型是内置类型或者自定义类型呢?
3.4 使用函数区分内置类型和自定义类型
因为内置类型的个数是确定的,可以将所有的内置类型集合在一起,如果能够将所拷贝对象确定下来,在内置类型集合中查找是否存在即可确定所拷贝类型是否为内置类型
//POD:plain old data 平凡类型 (无关痛痒的类型)————基本类型 //指在C++中与C兼容的类型,可以按照C的方式处理 bool IsPODType(const char* strType) { const char* arrType = {"char", "short", "int", "long", "long long", "float", "double", "long double"}; for (size_t i = 0; i < sizeof(arrType)/ sizeof(arrType[0]; ++i)) { if (0 == strcmp(strTypr, arrTypr[i])) { return true; } return false; } } template <class T> void Copy(T* dst, const T* src, size_t size) { if (IsPOPType(typeid(T).name())) { memcpy(dst, src, szieof(T)*size); } else { for (size_ i = 0; i <size; i++) { dst[i] = src[i]; } } }
通过typeid来确定拷贝对象的实际类型,然后在内置类型集合中枚举是否出现过,即可确定所拷贝元素的类型为内置类型或者自定义类型。但缺陷是:枚举需要将所有类型遍历一遍,每次比较都是字符串的比较,效率比较低。
3.5 类型萃取
为了将内置类型和自定义类型区分开,给出以下两个类分别代表内置类型和自定义类型
//代表内置类型 struct TrueType { //静态是为了可以使用类名直接调用该成员函数 static bool Get() { return true; } }; //代表自定义类型 struct FalseType { static bool Get() { return false; } };
给出以下类模板,将来用户可以按照任意类型实例化该类模板。
template <class T> struct TypeTraits { typedef FalseType IsPODType; };
对上述的类模板进行以下方式的特化:
template<> struct TypeTraits<int> { typedef TrueType IsPODType; }; struct TypeTraits<char> { typedef TrueType IsPODType; }; struct TypeTraits<short> { typedef TrueType IsPODType; }; struct TypeTraits<long> { typedef TrueType IsPODType; }; struct TypeTraits<long long> { typedef TrueType IsPODType; }; struct TypeTraits<unsigned int> { typedef TrueType IsPODType; };
通过对TypeTraits类模板特化的方式改写Copy函数模板,来确定拷贝对象的实际类型
/* T为int:TypeTraits<int>已经特化过,程序运行时就会使用已经特化过的TypeTraits<int>,该类中的IsPODType刚好为类TrueType,而TrueType中的Get函数就会返回true,内置类型使用memcpy方式拷贝 T为string:TypeTraits<string>没有特化过,程序运行时使用TypeTraits类模板,该类模板中的ISPODType刚好为类FalseType,而FalseType中Get函数返回true,自定义类型使用赋值方式拷贝 */ template <class T> void Copy(T* dst, const T* src, size_t size) { if (TypeTraits<T>::IsPODType::Get()) { memcpy(dst, src, sizeof(T) * size); } else { for (size_t i = 0; i < size; i++) { dts[i] = src[i]; } } }
3.6 STL中类型萃取的例子
// 代表内置类型 struct __true_type {}; // 代表自定义类型 struct __false_type {}; template <class type> struct __type_traits { typedef __false_type is_POD_type; }; // 对所有内置类型进行特化 template<> struct __type_traits<char> { typedef __true_type is_POD_type; }; template<> struct __type_traits<signed char> { typedef __true_type is_POD_type; }; template<> struct __type_traits<unsigned char> { typedef __true_type is_POD_type; }; template<> struct __type_traits<int> { typedef __true_type is_POD_type; }; template<> struct __type_traits<float> { typedef __true_type is_POD_type; }; template<> struct __type_traits<double> { typedef __true_type is_POD_type; }; // 注意:在重载内置类型时,所有的内置类型都必须重载出来,包括有符号和无符号,比如:对于int类型,必须特化三个,int -- signed int -- unsigned int // 在需要区分内置类型与自定义类型的位置,标准库通常都是通过__true_type与__false_type给出一对重载的函数,然后用一个通用函数对其进行封装 // 注意:第四个参数可以不提供名字,该参数最主要的作用就是让两个_copy函数形成重载 template<class T> void _copy(T* dst, T* src, size_t n, __true_type) { memcpy(dst, src, n*sizeof(T)); } template<class T> void _copy(T* dst, T* src, size_t n, __false_type) { for (size_t i = 0; i < n; ++i) dst[i] = src[i]; } template<class T> void Copy(T* dst, T* src, size_t n) { _copy(dst, src, n, __type_traits<T>::is_POD_type()); }
4. 模板分离编译
4.1 什么分离编译
一个程序(项目)由若干源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件链接起来形成单一的可执行文件的过称为分离编译模式
4.2 模板的分离编译
假如有以下场景,模板的声明与定义分离开,在头文件中进行声明,源文件中完成定义:
// a.h template<class T> T Add(const T& left, const T& right); // a.cpp template<class T> T Add(const T& left, const T& right) { return left + right; } // main.cpp #include"a.h" int main() { Add(1, 2); Add(1.0, 2.0); return 0; }
分析:C/C++程序要运行,一般要经历以下步骤:
预处理——>编译——>汇编——>链接
编译:头文件不参与编译,编译器对于工程中的多个源文件是分离开单独编译的
对程序进行词法,语法,语分析,错误检查无误后生成汇编代码,生成.obj文件
链接:将多个.obj文件合并成一个,并没有处理解决的地址问题
对于a.cpp文件来说,在编译期间在文件当中没有调用该模板函数的代码。所以就没有实例化,也就不会产生代码,不会生成具体的加法函数。
链接的时候,main.obj中调用Add<int>和Add<double>的时候编译器在该文件中没有找到就回去链接到其他的源文件中去查找,但是a.obj文件中没有实例化,所以就找到不就会报错。
4.3 解决方法
将声明和定义放到一个文件"xxx.hpp"里面或者"xxx.h"都可以。推荐使用。
将声明和定义放到一个头文件中,在预处理的时候该头文件就会在源文件(main.cpp)中展开,然后对于main.cpp文件进行编译的时候,就可以直接在main.cpp文件中进行实例化了,从而就会产生相应的加法函数的代码。
而不是像预处理的时候,只声明了加法函数模板在其他的源文件(a.cpp)中定义这呢,但是a.cpp文件中没有调用该模板函数的代码也就不会实例化出加法函数,所以在编译的时候main.obj就会产生链接不到加法函数的错误
模板定义的位置显式实例化。这种方法不实用,不推荐。
5. 模板总结
【优点】:
模板复用了代码,节省资源,更快的迭代开发,C++标准模板库(STL)因此而产生
增强了代码的灵活性
【缺点】:
模板会导致代码膨胀问题,也会导致编译时间变长
出现模板编译错误时,错误信息非常凌乱,不易定位错误