科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道 C++的泛型编程和限制参数类型的技术探讨

C++的泛型编程和限制参数类型的技术探讨

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

泛型是C++中的重要特性。据说,已经在C++社区中已经取代面向对象成为C++的主要编程泛型。STL和boost库等都广泛使用了泛型。

作者:良少 来源:blog【原创】 2008年1月14日

关键字: 特化 泛型 模版 C++ 软件

  • 评论
  • 分享微博
  • 分享邮件
模板概述
泛型是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++的模板要是也这样就好了!
 
 
    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章