首页 > 代码库 > COM是一个更好的C++

COM是一个更好的C++

昨天看了《COM本质论》的第一章”COM是一个更好的C++”,认为非常有必要做一些笔记,于是整理成这篇文章,我相信你值得拥有。

这篇文章主要讲的内容是:一个实现了高速查找功能的类FastString,在一个小小的需求之后,慢慢的演变成一个COM组件的过程。

类FastString实现了一个高速查找字符串的功能,快到时间复杂度是O(1),我们先无论作者是怎么实现的,预计是通过空间换时间。因为这个类查找字符串非常快,于是作者就把这个类当做一个产品,以源代码的方式卖给须要的厂商,厂商用后感觉非常好,但有的厂商想要获得字符串长度的功能,他们认为strlen(str)速度太慢,毕竟这个函数获取字符串的长度是线性的,时间复杂度是O(N),于是作者决定改动他的FastString,其内心一直在告诉自己:我的FastString必须是Fast。

我们先来看看作者FastString的样子:

class FastString
{
public:
      FastString(const char* str);
      FastString(void ); 
      int Find (const char* str );
private:
      char* m_str ; 
};

可别小看这个类,它查找字符串可快了(我也不知道为什么它就他妈的这么快)。聪明的作者听了厂商的需求之后,非常快的就想到了非常好的解决方式,通过一个变量len来存字符串的长度,通过一个函数Length返回变量len,时间复杂度但是O(0)哦,于是作者非常快的实现了厂商的需求,大概例如以下:

class FastString
{
public:
      FastString(const char* str);
      FastString(void );
      int Length ();//新增的
      int Find (const char* str );
private:
      char* m_str ;
      int len ;//新增的
};

在经过天衣无缝的測试之后,作者骄傲的将他的作品分发给了愿意再次掏钱的厂商,厂商用了非常是火大,出现了各种莫名其妙的问题,在被各个厂商咆哮之后,作者发现了他的作品的缺陷,于是决定走上COM之路

 

我们先来看看厂商用了作者的FastString之后为什么就挂了呢?

厂商们拿了作者的源代码之后,就以源代码的方式和自己的其它代码一起编译成一个DLL文件,然后让自己的产品升级,升级就是简单的覆盖这个DLL文件,于是厂商的产品升级之后就挂了。由于FastString可能在多个DLL中多个文件都实例化了,在这些DLL中FastString占用4个字节的内存,而新版本号的FastString占用的是8个字节的内存,厂商仅仅覆盖了FastString所在的DLL,而没有覆盖全部使用了FastString的DLL,由于FastString所在的DLL创建FastString是8个字节,而其它DLL中是4个字节,假设跨库传递FastString,将一个4字节的对象当做一个8字节的对象来用,这还不挂。

聪明的作者非常快就实现了他的COM组件,源代码大概是以下这个样子,不要奇怪为什么作者的COM之路这么顺风顺水,这么快就出了作品。

#pragma once
class IExtensibleObject
{
public:
    virtual void* Dynamic_Cast(const char* str)=0;
    virtual void AddRef()=0;
    virtual void Release()=0;
};

class IFastString:public IExtensibleObject
{
public:
    virtual int Length(void)=0;
    virtual int Find(const char* str)=0;
};

class FastString:public IFastString
{
public:
    FastString(const char* str=NULL);
    virtual void* Dynamic_Cast(const char* str);
    virtual void AddRef() ;
    virtual void Release();
    virtual int Length();
    virtual int Find(const char* str);
    ~FastString();
private:
    char* m_str;
    int len;
    int m_cPtrs;//引用计数
};

//导出函数
extern "C" __declspec(dllimport) IFastString*  CreateFastString(const char* psz);

作者的COM组件做到了一下几点,最终实现了增量更新。

1:作者不在以源代码的方式卖给厂商,而是以头文件和库的方式卖个厂商,厂商能够通过静态/动态的方式链接作者的库。

2:作者不在让厂商到处实例化他的FastString,我可爱的FastString。而是通过一个导出函数实例化FastString,并返回IFastString,这样就不会出现不同DLL中FastString实例大小不一样的问题。如今全部的实例都在作者的DLL中创建了。

3:关于回收FastString的问题?作者刚開始是想直接delete掉CreateFastString返回的指针,但为了实现COM组件,此时的FastString已经不是彼时的自己了,他继承并实现了多个接口,因为接口之间转换来转换去,都不知道删除哪个指针了,于是作者决定通过使用引用计数的方式销毁FastString。

4:为什么要自己实现Dynamic_Cast?

RTTI是一个与编译器极为相关的特征,每一个编译器厂商对RTTI的实现是独有的,这大大破坏了“以抽象基类作为接口而获得的编译器独立性”,既然每一个编译器可能有不同的实现,即析构函数不能定义成虚函数,由于不同的编译器,虚函数在虚方法表中的位置是不一样的,有的编译器放在最前面有的放在最后面,这会导致不同的编译器编译后虚方法在虚方法表中的位置是不一样的。所以析构函数不能定义成virtual,其它public接口都必须定义成virtual。其它虚方法在虚方法表中的位置和虚方法的声明保持一致,即依照声明的顺序存放在虚方法中。

因为类型转换和引用计数是每一个接口都须要的,于是把他们提出来放到最顶层,让全部的接口继承它。

5:新增的接口仅仅能加在最后面,废弃的接口不能删除。

假设新增的接口插在中间,那么部分接口在虚方法表中的地址就会发生变化,新版本号的DLL就不能与已经公布的程序兼容,就不能实现增量升级,即仅仅用覆盖某个DLL,而不须要所有都要更新,废弃的接口删除会导致相同的问题。

 

综述:为什么作者的这个DLL能实现增量更新?

COM对象通过特定的导出方法在DLL中以new的方式创建,通过引用计数自己主动析构,client不能自己创建COM对象,COM对象的内部结构发生变化,对外部也没有影响,假设新增了接口,就在最后加,之前的接口在虚方法表中的位置不会受到印象,即对别的接口没有影响,废弃的接口不能删除,

改变对象的内存结构和新增virtual方法都没关系,那不就成了。实现增量不在是问题,我们在回到FastString这个问题上,假设FastString一開始是以上诉方式实现的,如今要新增一个len字段和一个Length接口,我就这样增了,新出个版本号,直接覆盖曾经的那个DLL,我直接能够用,一切都是OK的,外部的调用不会受到不论什么影响。为了证明这个FastString能实现增量升级,我做了一个DEMO,大家能够试一下,我就是下载地址

你也许会说我这说的都不是COM,但这的确是更好的C++。

我的博客文件夹