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