模板概述
泛型是C++中的重要特性。据说,已经在C++社区中已经取代面向对象成为C++的主要编程泛型。STL和boost库等都广泛使用了泛型。
泛型,就是C++的模板机制。
模板可以看作是C++宏的衍生。宏,就相当于是文本文件中的替换。C++编译器在编译前,先把所有使用宏的地方,用宏的定义替换掉宏。
在Java,.net,ruby等现代语言中都没有宏这种语法的地位。
宏是另程序变得晦涩难懂的一个原因!我认为在程序中应该尽量避免使用宏!
模板也可以看作是一种模板。C++编译器在编译之前,将创建模板的具体类型的源代码,然后再编译成二进制代码。
模板技术
模板类的声明和定义,形如:
template<typename T> class Manage{…全部内联函数实现!};
函数模版的定义,形如:
template<typename SequenceT>
void trim(SequenceT &, const std::locale & = std::locale());
模板的特化
模板类的特化
1)首先定义基泛型:
template<typename T> class Manage{…全部内联函数实现!};
2)然后定义特化的泛型:
#include 上面基泛型的文件
template<> class Manage<B>
{…全部内联函数实现!};
特化的泛型必须自己实现所有基泛型定义的成员函数和静态成员。
模板类的成员函数的特化
如果我们希望特化的泛型继承绝大部分的基泛型的代码。
那么只需定义特化的成员函数即可!
在基泛型的定义后面加上特化成员函数:
template<>
void Manage<B>::sayHello(void){
cout<<"B"<<this->t<<endl;
};
这个特化的函数就是特化模板类的成员函数。
实际上,这相当于是隐式定义了上面的那样一个特化模板类,并且所有的基本实现使用基泛型模板的实现!
偏特化/部分特化
就是一个模板类有多个泛型参数。
我们特化一个模板参数:
1)基泛型有多个模板参数:
#pragma once
#include "cppunit/extensions/HelperMacros.h"
#include "B.h"
#include <iostream>
using namespace std;
template<typename V,typename T> class Manage
{
private:
T* t;
public:
Manage(void){
this->t=new T();
};
void sayHello(void){
cout<<"管理"<<this->t<<endl;
};
public:
virtual ~Manage(void){};
};
2)定义的特化有一个还是任意的类型参数
#pragma once
#include "Manage.h"
#include "B.h"
#include <iostream>
/*相当于
template<typename V,没有> class Manage<V,B>
没有对应已有的类型B
*/
template<typename V> class Manage<V,B>
{
private:
B* t;
public:
Manage(void){
this->t=new B();
}
virtual ~Manage(void){};
public:
void sayHello(void){
std::cout<<"B类"<<this->t<<std::endl;
};
};
但是,请注意,半特化,则没有特化中对应的成员函数的特化那种简单扼要的形式!!!
模板的使用
使用模板的类应该写在头文件中,并以源码的方式发布
C++的泛型编程中,需要把所有使用到泛型声明或者定义的代码都直接写在.h头文件中,不能写在.cpp文件中,否则会有很多奇怪的错误!
VC2005也还没有支持分离编译的export关键字!
模板类只能写在一个.h文件中。而且,不可以放在dll项目中。因为模板类是无法导出的!
导出以后的模板类,只能够在外部声明这个模板类,不能够实际创建模板类的对象!否则会报告
TestMain.obj : error LNK2019: 无法解析的外部符号"__declspec(dllimport) public: __thiscall net_sf_interfacecpp_core_lang::ObjectRefManage<class AClass>::ObjectRefManage<class AClass>(void)" (__imp_??0?$ObjectRefManage@VAClass@@@net_sf_interfacecpp_core_lang@@QAE@XZ),该符号在函数_main 中被引用
这样的错误。
因为,模板类实际上并没能编译成二进制代码。它只是一个宏!需要在编译时根据客户代码的使用情况生成源代码,然后再变成二进制代码。
因此,作为宏,它应该在.h文件中。作为源代码的元数据,应该共享给用户。因为它需要根据客户的使用情况来生成源代码。因此,它必须在最终客户代码一起!
要使用模板类,就必须把它单独拿出来,把.h这个头文件/源代码交给用户。
用户在项目中直接作为源代码使用这个头文件,才能够使用这个模板类!
//确保只被引入系统一次
#ifndef _net_sf_interfacecpp_core_lang_ObjectRefManage_h_
#pragma once
#include "..\net_sf_interfacecpp\IObject.h"
//下面是自定义的所有.cpp文件都需要引入的头文件
//#include "ConfigApp.h"
#include "..\net_sf_interfacecpp\Object.h"
#pragma comment(lib,"..\\debug\\net_sf_interfacecpp.lib")
/*
用于管理任意类的实例的生命周期,使之符合IObject接口
模板类必须定义在头文件中
NET_SF_INTERFACECPP_API
*/
namespace net_sf_interfacecpp_core_lang{
template<typename T>
class ObjectRefManage:public IObject
{
private:
IObject* pIObject;
T* pT;
//copy构造函数
ObjectRefManage(const ObjectRefManage &that);
//重载等于操作符
ObjectRefManage& operator=(const ObjectRefManage &that);
//void operator delete(ObjectRefManage* thisPtr);
public:
T* getObjectPtrAndAddRef(){
this->addRef();
return this->pT;
};
T* getObjectPtrNotAddRef(){
return this->pT;
};
ObjectRefManage(void){
//现在引用是
this->pIObject=new Object();
this->pT=new T();
};
long addRef(){
return this->pIObject->addRef();
};
long release(){
long result=this->pIObject->release();
if(result==0){
delete this->pT;
delete this;
return 0;
}
};
void setSingleton(){
this->pIObject->setSingleton();
};
public:
virtual ~ObjectRefManage(void){};
};
}
//确保只被引入系统一次
#define _net_sf_interfacecpp_core_lang_ObjectRefManage_h_
#endif
dll依赖模板时使用方式
1)模板依赖于我们的dll
2)如果我们的类需要使用这个模板,就需要另外建一个dll—ext.dll,包括这个模板,从而间接包括核心dll。
Dll内部时可以使用模板的,因为可以直接在生成dll时根据内部的使用模板的情况,创建源代码,编译成dll。
但是,如果把dll内部的模板发布出去,这就不行了!
3)这个模板头文件和dll必须同时提供,避免找不到模板依赖的dll而出错!
对模板参数没有限制是一大误区
考察STL和boost中使用泛型的例子。我发现一个问题。使用模板的类,在使用时,程序员可以指定任何类和基本类型。
但是,实际上,很多模板类在代码的内部实现中,对参数类型能够提供的操作实际上是有要求的。如,需要>,<,=等操作是有意义的。
或者需要能够调用某个方法。
但是,STL和boost的库中,均没有对参数进行限制!
这样,如果客户程序员使用了错误的参数类型,那么程序还是能够正常编译。只有在运行到这段代码时,才会报错。
甚至,由于STL和boost喜欢使用操作符重载,因此,即使运行时,也不会出错,只是真正的逻辑错了。这样的问题,怎么才能找到错误点呢?我不禁倒吸了一口凉气!
翻开C++之父BS的《C++语言的设计与演化》一书,BS本人对模板的这一描述,令我乍舌!
BS居然认为不需要限制模板的参数类型。认为对模板参数的限制是OOP程序员的偏见!
晕!C++是静态编译型语言,不是ruby,python,JavaScript这样的动态面向对象语言。
如果ruby开发中,你用了错误类型的对象,执行时没有报错,直到你运行到这段代码才报错,那我也没什么话好说的。人家是解释型语言,放弃了编译检查错误,但换来了语言的巨大动态灵活性。有所得必有所失嘛!这我就不说它了!
但BS认为C++不应该限制模板的参数类型,听任错误在运行时爆发,就让我无法理解了!
BS,不能因为你对模板的偏爱,让这么多C++程序陷入危险啊!
通过派生对模板的参数类型加以限制的一种方法。
形如:
Template <typename T> class Compare{};
Template <typename T: Compare > class Vector{};
BS认为不应该采用这种方式。
在java中使用模板时,我们经常使用这种方式。
如:
Public MyClass<E extends String>{
……
}
但,BS认为这种方式不好。而且我在VS2005中也无法编译这样的代码。
确实,这样会让模板类的数量直线上升。
第二种BS提到的方法非常丑陋。
就是让每一个方法的实现都转换成我们需要的类型。这样编译时就会报错。
第三种方法,就是使用模板的特化,或者叫做专门化。
这是BS推荐使用的方法。我也认为应该使用模板特化来限制模板的参数类型。
尽管BS提出这种语法的本意并不是用来限制模板的参数类型。
因为,BS根本就不认为应该限制模板的参数类型。偏执的家伙!
使用模板特化限制模板的参数类型
作为一个坚定的OO程序员,我是不会容许在自己的C++程序中像STL和boost那样,允许任意参数类型随意使用我的模板类的!
BS的观点,我不能苟同!
我认为,可以使用模板特化限制模板的参数类型。这种办法是最简单有效的。
首先,我们定义一个基范型。
然后再在基范型模板类的外部定义几个重载的方法。
指定如果是我们需要的参数类型,应该执行这些方法。
也可以独立定义特化的模板类。但是,我们上面已经说过了,特化模板类,不如特化模板类的成员函数合算!
最后,我们在基范型的实现中,抛出一个自定义的异常。这样,如果使用了错误的类型,就会抛出异常,导致系统停止运行。我们的客户就可以发现问题所在。
当然,编译时,即使是不正确的类型,还是能够编译通过。只有在运行时才会把错误抓出来。
编译时检查不出错误,这只能怪BS和C++标准委员会没有为我们提供限制模板的参数类型的语法了。
补充:C++的模板和java的模板的异同
Java5中,也引入的泛型语法。如:
Public MyClass<E extends String>{
……
}
看上去类似,但是实际实现却非常不同。
C++的模板,是会在编译时,先生成很多新的C++类。因此,C++中使用模板有一个问题,就是模板生成的源代码可能太多。引起编译的性能问题。
而java的模板实现机制完全不同。Java模板类在编译时,会“擦除”类型信息。
不会生成新的java类的源代码。
因为,java的类继承体系是单根的,所有类都是Object类的子类。因此,在Java5之前,没有引入模板这个语法之前,java和它的集合实现类也过得很滋润。
Java模板类在编译时,我猜想是这样子的:
1,首先,擦除模板类型的信息,还是使用原来的Object类型。
2,在所有使用模板的参数类型的地方,加上强制类型转换,转换成程序员指定的模板参数类型。
我特别记得BS的一句话,认为特有道理:
他在C++中特别把不应该使用的语法设计得丑陋,让你不想去使用。如:
dynamic_cast < type-id > ( expression )
动态类型转换。
BS认为,显式的类型转换通常是不必要的。应该避免。
我深深地赞同这句话。Java引入模板,应该就是为了这个原因。现在,写Java代码可以少用很多强制类型转换!
用错模板的参数类型,Java编译器都会准确地报告错误。
唉,C++的模板要是也这样就好了!