C++ 泛型编程 模板
前言
0. 所谓泛型编程就是独立于任何特定类型的方式编写代码,使用泛型程序时,需要提供具体程序实例所操作的类型或者值。我们经常用到STL容器、迭代器、和算法都是泛型编程的例子;
- 模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型;
- 模板是一种对类型进行参数化的工具;
- 通常有两种形式:函数模板和类模板;
- 函数模板针对仅参数类型不同的函数;
- 类模板针对仅数据成员和成员函数类型不同的类;
- 使用模板的目的就是能够让程序员编写与类型无关的代码。比如编写了一个交换两个整型int 类型的swap函数,这个函数就只能实现int 型,对double,字符这些类型无法实现,要实现这些类型的交换就要重新编写另一个swap函数。使用模板的目的就是要让这程序的实现与类型无关,比如一个swap模板函数,即可以实现int 型,又可以实现double型的交换。
模板
1) 模板定义很特殊。由template<…>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直处于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉指定模板的多重定义。所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。
2) 在分离式编译的环境下,编译器编译某一个.cpp 文件时并不知道另一个.cpp 文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的.obj中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。
模板函数
普通定义形式
template<typename T> int func(const T &a1, const T &a2){ ... } template <class T> inline int func(const T &a1, const T&a2){ ... } template<typename T1, typename T2, typename T3> T1 func(const T2 &t2, const T3 &t3){ ... } //调用方法 func<long>(i, log);
如果类型的定义顺序与调用顺序不一样的话, 则需要在申明的时候制定类型顺序;
tempalte<typename T1, typename T2, typename T3> T3 func(const T1 &t1, T2 &t2) { ... } //调用方法 func<long, int , long>(12, 34);
- 编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
- 这是因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误。
模板类
//定义方式 template <class Type> class Queue{ ... ... }; //使用方法 Queue<int> qi;
- 模板形参表不能为空;
- 由编译器根据实参列表实例化模板, 模板在实例化的时候会对参数类型进行语法检查;
- 作用域由声明之后直到模板声明或定义的末尾处使用;
- 模板形参的名字只能在同一模板形参表中使用一次;
- 声明和定义的模板形参名称可以不一样;
- 模板类型形参可以作为类型说明符用在模板中任何地方,与内置类型说明符或类类型说明符的使用方式完全相同;
- 对模板的非类型形参而言, 求值结果相同的表达式将认为是等价的,调用相同的实例;
- 编写模板代码时,对实参类型的要求尽可能少;
非类型模板形参
template <class T, size_t N> void func(T (&parm)[N]) { //此时,N将直接用值代替 }
关于模板编译
发现错误一般分为三个阶段:
1. 编译模板定义本身,可以检测模板本身的语法问题,例如漏掉分号,拼写错误等;
2. 编译器见到模板的使用时,检测参数个数、类型是否合法;
3. 模板实例化期间,检测类型相关的错误;
模板实例化
类模板在引用实例类模板类类型时实例化,函数模板在调用它或者用它对函数指针进程初始化或者赋值时实例化,在使用函数模板时,编译器通常会为我们推断模板实参;
当编译器看到模板定义时,不立即产生代码, 只有在看到模板调用时,编译器才会产生对应的实例,类型相关的错误才会被检查出来。
函数模板的实例化是由编译程序在处理函数调用时自动完成的,而类模板的实例化必须由程序员在程序中显式地指定。即函数模板允许隐式调用和显式调用而类模板只能显式调用。在使用时类模板必须加<T>,而函数模板不必。
模板编译模型
通常情况下,实例化一个对象或者调用一个函数时,编译器不需要看到函数或者类的定义,只有在连接的时候才会去关心类或者函数的定义。但是模板不一样, 编译器在实例化模板时,必须看到模板的定义才会编译通过。
包含编译模型
//header file #ifndef xx_H_ #define XX_H_ template <typename T> int func(T &t1, T&t2); #include "oo.cpp" //模板定义文件 #endif //oo.cpp template<typanem T> int func(T &t1, T&t2) { ... }
类模板成员函数
- 必须以关键字template开头, 后接类的模板形参表;
- 必须指出它是哪个类的成员;
- 类名必须包含其模板形参;
- 类模板的成员函数本身也是模板函数,像任何其他函数模板一样,需要使用类模板的成员函数产生改成员的 实例化,也就是说只有在使用的时候才会被实例化;
- 类模板的形参定义在实例化类的时候确定了,所以调用的时候用于进行参数的常规类型转换;
template <class T> ret-type Queue<T>::member_func_name { //define }
非类型形参的模板实参
- 非类型模板实参必须是编译时常量表达式
template <int hi, int wid>
class Screen
{
public:
Screen():{}
private:
std::string screen;
std::string::size_type cursor;
std::string::size_type height, width;
};
//实例化方法,参数必须是编译时常量表达式
Screen<24, 80> hp2621;
类模板中友元声明
- 普通友元,将由原关系授予制定的类或函数
template <class Type> class Bar { friend class FooBar; ... }
FooBar 的成员可以访问Bar类任意实例的private &protected 成员。
- 一般模板友元关系
template <class Type> class Bar { template <class T> friend class Fool; template <class T> friend void templ_fcnt(const T&); ... };
表示Fool和templ_fcnt的任意实例都可以访问Bar的任意实例的private和protected成员。
- 特定模板友元关系
模板类只授权对特定友元实例的访问权
template <class T> class Foo2; template <class void templ_fcnt(const T&); template <class Type> class Bar { friend class Foo2<char*>; friend void templ_fcnt<char*>(char *const &); }
更通用的形式
template <class T> class Foo2; template <class T> void templ_fcnt(const T&); template <class Type> class Bar { friend class Foo2<Type>; friend void templ_fcnt<Type>(const Type &); ... }
这样每个类型的类模板实例与对应的类型友元建立了一一映射关系。
- 声明依赖性
如果模板类授权给所有友元实例访问private和protected成员时, 编译器将友元声明当做类或者函数的声明对待;但是如果指定到特定类型时,必须在前面声明类或者函数。参考上面特定模板友元关系 和 一般友元关系 的声明。
同时,如果没有提前告诉编译器该友元是一个模板,编译器则认为友元是一个普通非模板函数或者非模板类。
模板类的成员模板
这个名字确实有点绕, 其本质意思就是模板类的成员函数也希望有自己的参数类型,看如下例子:
template <class Type> class Queue { public: template <class It> Queue(It begin, It end): head(0), tail(0) { copy_elems(beg, end); } template <class Iter> void assign(Iter , Iter); private: template <class Iter> void copy_elems(Iter, Iter); }
在类模板的外部定义模板成员,必须包含类模板的形参和模板成员的模板形参:
template <class Type> //类模板的形参 tmeplate <class Iter> //成员模板形参 void Queue<Type>::assign(Iter begin, Iter end) { ... }
与其他成员一样,成员模板也只有在被使用的时候才会实例化。
类模板的static成员
template <class Type> class Bar { public: static std::size_t count(){return ctr}; private: static std::size_t ctr; }
实例化原则是:相同类型的实例共享一个static成员,例如Bar 类型的实例共享一个static 成员ctr,Bar 类型的实例共享一个static成员ctr;
- 使用方法
Bar<int> bar1, bar2; size_t ct = Bar<int>::count();
- 初始化方法
template <class Type> size_t Foo<Type>::ctr = 0;
模板特化
由于模板的定义中,其操作都是依赖实例化的类型是否支持该操作或者操作的结果与预期是否相匹配,例如:
template <class Type> int compare(const Type& t1, const Type &t2) { if(t1 > t2) return 1; if(t1 < t2> return -1; return 0; }
在上面的例子中,如果用char* 去实例化模板时,函数将比较两个指针,很明显与预期的记过不相吻合。此时可以通过模板特例话来解决。
函数模板特化
函数模板特例化形式如下:
- 关键字template 后面接一对空的尖括号(<>);
- 在接末班吗和一堆尖括号,尖括号中制定这个特化定义的模板形参;
- 函数形参表;
- 函数体。
例如:
template <> int compare<const char*>(const char *t1, const char *t2) { return strcmp(t1, t2); }
如果有多个模板形参,则依次排列即可。
类模板特化
- 定义类特化
template <> class Queue<const char*> { ... }
需要在类特化的外部定义普通成员函数时,成员之前不能加 template<>标记:
void Queue<const char*>::push(const char* val) { ... }
- 特化成员而不特化类
template <> void Queue<const char *>::push(const char* const &val) { ... } template<> void Queue<const char*>::pop() { ... }
现在,类类型Queue
template <class T1, class T2> class tem { ... }; //partial specialization :fixes T2 as int and allows T2 to vary. template <class T1> class tem<T1, int> { }
使用方法:
tem<int , string> foo; //调用普通的类模板 tem<string , int> bar; //调用偏特化版本
重载与函数模板
函数模板可以重载:可以定义有相同名字但形参数据或类型不同的多个函数模板, 也可以定义与函数模板有相同名字的普通非模板函数。
不过从实践来看,设计既包含函数模板又包含非模板函数的重载函数集合是困难的,因为坑你会使函数的用户感觉到奇怪,定义函数模板特化几乎总是比使用非模板版本更好。