zoukankan      html  css  js  c++  java
  • C++ > Com

    从C++转变到COM
    从纯C++开发转移到COM的世界似乎受很大限制。很多你所知道的和钟爱的语言结构已经
    从你的知识库中消失了,取而代之为一些在一种更接近于C而不是C++的语言中被称之为
    属性的全新语言结构。典型的C++开发者的思考方法着重放在对象的实现上。要使程序员
    的注意力转变到根据通过请求和响应消息来通信的组件来思考问题,这需要花费相当多
    的时间。这一章将讨论一些重要的观念转变。而观念的转变将有助于你从"100%纯"C++转
    变为另一种风格的子集,也就是我们将认识的组件对象模型成为可能所必须的。
    1  在定义你的对象之前定义你的接口(在IDL中实现)
    C++开发者的许多基本反应之一,就是在一个".h"文件中开始一个项目编码阶段。也就是
    ,在这里C++的开发者普遍地开始定义他的数据类型的公共操作以及它们的内部表现形式
    。当开发一个纯基于C++的项目时,这是一个完全合理的方法。然而,当开发一个基于C
    OM的项目时,这种方法通常会带来痛苦和麻烦。
    COM中最基本的概念是接口和实现的分离。虽然C++程序语言支持这种类型的编程,但它
    对于显式的把定义接口作为一个单独的实体,从实现他们的对象中分离出来的支持很少
    。如果没有了接口的支持,就很容易模糊接口和实现的界限。对于COM开发新手来说,通
    常容易忘掉接口是用来对一些功能进行抽象定义的。这意味着定义一个COM接口不应该违
    背实现接口的一个特定对象的实现细节。要考虑到以下的C++对象的定义:
    class Person {
      long m_nAge;
      long m_nSalary;
      Person *m_pSpouse;
      list <Person*> m_children;
    public:
      Person(void);
      void Marry(Person& rspouse);
      void RaiseSalary(long nAmount);
      void Reproduce(void);
      Person *GetSpouse(void) const;
      long GetAge(void) const;
      long GetSalary(void) const;
      const list<Person*>&GetChildren(void) const;
    };
    这是一个完全合理的类定义。然而,如果我们把它当作一个COM接口定义的起点,那么最
    直接的映射看上去应该是这样:
    DEFINE_GUID(IID_IPerson, 0x30929828, 0x5F86, 0x11d1,
             0xB3, 0x4E, 0x00, 0x60, 0x97, 0x5E, 0x6A, 0x6A);
    DECLARE_INTERFACE_(IPerson, IUnknown) {
      STDMETHOD(QueryInterface)(THIS_REFIID r,void**p)PURE;
      STDMETHOD_(ULONG, AddRef)(THIS) PURE;
      STDMETHOD_(ULONG, Release)(THIS) PURE;
      STDMETHOD_(void, Marry)(THIS_IPerson *pSpouse) PURE;
      STDMETHOD_(void, RaiseSalary)(THIS_long nAmt) PURE;
      STDMETHOD_(void, Reproduce)(THIS) PURE;
      STDMETHOD_(IPerson *, GetSpouse)(THIS) PURE;
      STDMETHOD_(long, GetAge)(THIS) PURE;
      STDMETHOD_(long, GetSalary)(THIS) PURE;
      STDMETHOD_(list<IPerson*> *, GetChildren)(THIS) PURE;
    };
    DECLARE_INTERFACE_宏,用于强调这个接口定义必须出现在C/C++的首文件中。这种接口
    定义的第一个缺陷,就是它运用了一个标准模板列表来返回孩子的集合。虽然这种方法
    在一个封闭的单一的二进制系统中,即所有组成部分的源代码将被编译和连接成一个不
    可分割的单元,是合理可行的。但是,这种技术在COM中将是致命的。因为,在COM中,
    组件可能在一种编译器下构造,却被不同编译器编译的客户代码所使用。在这种情况之
    下,就不可能保证客户和对象在一个STL列表的表现形式是一致的。即使两个实体是用同
    一个销售商所提供的编译器来编译,也不能保证他们使用的是同一个STL版本①。
    与返回STL列表有关的一个更明显的问题是,这个接口有可能妨碍潜在对象细节的实现,
    即它使用一个STL列表来存储它的孩子的集合。对于前面所提供的实现,这不是一个问题
    。然而,如果其他的对象实现者希望来实现这个IPerson接口,他们同样必须把孩子存放
    在STL列表中或者在每次调用GetChildern方法时创建一个新的列表。既然STL列表并不是
    一个集合的最有效的表现形式,那么这个约束给所有的IPerson实现带来了一个不小的负
    担。
    与数据类型有关的最后一个缺点跟每个方法的返回结果有关。每个IPerson方法都是使用
    STDMETHOD_宏来定义的,它允许接口设计者指明方法的物理结果类型。因此,用下面的
    方法定义:
    STDMETHOD_(long, GetAge)(THIS) PURE;
        经C预处理器处理,它将被扩展为以下代码:
    virtual long __stdcall GetAge(void) = 0;
    这种方法存在的问题是,它不能返回一个HRESULT值。COM通过重载方法返回的实际值类
    型来指示通信的失败;而这种方法不返回一个HRESULT值,所以COM无法通知客户可能发
    生的通信错误。
    如果开发者使用COM的接口定义语言(IDL)来定义接口,那么上面所说的问题就不会出
    现。看一看IDL并且进行思考,你会发现它是很吸引人的。为什么我必须掌握另一门语言
    呢?存在这种疑惑是合理的。大多数25岁以上的开发者会因已经掌握了一种编程语言而
    不愿意学习另一种编写条件和循环的语法,这种不情愿是可理解的。因为,每一个阶段
    所产生的新的编程语言都承诺将会极大地提高程序员的生产力。然而,他们生产出来的
    往往是语法怪胎。
    必须着重指出的是,COM的IDL不是一种编程语言,对大多数人来说它并不是全新的。ID
    L没有用来编写可执行语句的结构。IDL是一种基于属性的描述性语言,它只拥有用来定
    义与COM兼容的数据类型的结构。同时,IDL从C语言那里继承了语法,这些语法是现在很
    多使用COM工作的C、C++和JAVA开发者所熟悉的。IDL语法是非常简单的,以至用其他语
    言(例如Visual Basic和面向对象的Pascal)工作的开发者,可以在一星期之内轻松地
    掌握那些基本的东西。的确,在将来一些COM+版本中,可能会用一种更为集成的方案来
    代替当前的IDL编译器。然而,即使MIDL.EXE被扔到回收站,IDL所使用的基于属性的技
    术也会依然存在。其前提是IDL仍被保留。
    IDL直接从定义枚举、结构和联合的C语言中继承了它的语法。下面的片断就是完全合理
    的IDL:
    enum COLOR { RED,  GREEN,  BLUE };
    struct ColorPoint  {
      enum COLOR color;  /*  color */
      long x;              /* horiz,  coord.  */
      long y;              /* vert.  coord.  */
    };
    struct NODE  {
      struct ColorPoint value;  /* value of node */
      struct NODE         *pNext;  /* pointer to next node */
    };
    这个IDL片段也是合法的C程序。应当注意的是,虽然它也是合法的C++程序,但IDL是使
    用C约定而不是C++约定从类型命名空间中分离标识命名空间。这意味着
    struct NODE {
        ColorPoint value;    // Not legal IDL
        NODE         *pNext;  // Not legal IDL
    };
    不是合理的IDL,因为ColorPoint和NODE名字必须用关键字struct来限定作用域。
    IDL给C语言增加的最主要扩展,就是既能定义COM接口,又能定义COM类的能力。下面的
    片段比先前所说的基于DECLARE_INTERFACE的更可取:
    [ uuid(30929828-5F86-11dl-B34E-0060975E6A6A), object ]
    interface IPerson  :  IUnknown  {
    import "unknwn. idl";
    void Marry([in] IPerson *pSpouse);
    void RaiseSalary([in] long nAmount);
    void Reproduce(void);
    IPerson * GetSpouse(void);
    long GetAge(void);
    long GetSalary(void);
    list<IPerson*> * GetChildren(void);
    }
    当这个接口定义被提交给IDL编译器时,先前所指出的问题将会导致IDL编译器产生错误
    信息。这个错误信息通知接口设计者他们的方法是错误的。
    例如,既然IDL假设接口将要跨进程和主机边界使用,那么它将确保所有的方法都返回H
    RESULT。同时,可以通过使用[local]属性来禁止这种检查(以及远程代码的生成)。
    [
       uuid (30929828-5F86-11dl-B34E-0060975E6A6A),
       object, local
    ]
    interface IPerson : IUnknown {
     ...
    不过,更好的方法是完全遵守COM的规则和约定,使用[retval]属性把物理参数映射给逻
    辑结果。
    [ uuid(30929828-5F86-11dl-B34E-0060975E6A6A), object ]
    interface IPerson : IUnknown {
       import "unknwn.idl";
       HRESULT Marry([in] IPerson *pSpouse);
       HRESULT RaiseSalary([in] long nAmount);
       HRESULT Reproduce(void);
       HRESULT GetSpouse([out,  retval]  IPerson **ppRes);
       HRESULT GetAge([out, retval]  long *pRes);
       HRESULT GetSalary([out,  retval]  long *pRes);
       HRESULT GetChildren([out,  retval]  list<IPerson*> **p);
    }
    这个更新版的定义现在只有一个地方阻碍成功编译:即使用了STL。
    GetChildren方法的参数是用STL列表来定义的,就像先前所说的那样,这种技术暴露了
    太多关于这个接口特定实现的信息。即使这不妨碍你,IDL编译器也不允许使用STL(或
    者任何C++特殊的类型)。在这期间,即使使用C预编译器来#include list<>的定义,
    IDL编译器也会由于这个文件中的C++语言风格(例如,模板以及成员函数定义)而阻塞
    (choke)。唯一可行的方法是,为这个结果选择一个与IDL以及COM相兼容的数据类型。

    如果所期望的实现语言是C或者C++,那么合理的方法(像在实例13所解释的那样)就是
    用一个枚举结构来模拟结果集(resultant collection)。
    // enumerator interface to model collection of children
    [ uuid(30929829-5F86-11dl-B34E-0060975E6A6A), object ]
    interface IEnumPerson :  IUnknown {
       import "unknwn.idl";
       HRESULT Next([in]  ULONG cElems,
                [in,  size_is (cElems),  length_is(*pcFetched)]
                                      IPerson **rgElems,
                             [out] ULONG *pcFetched);①
       HRESULT Skip ([ in] ULONG cElems);
       HRESULT Reset (void);
       HRESULT Clone([out]  IEnumPerson **ppEnum);
    }
    如果用上面的接口来代替的话,那么GetChildren方法可以定义如下:
    [ uuid(30929828-5F86-11dl-B34E-0060975E6A6A), object ]
    interface IPerson :  IUnknown {
       ...
       HRESULT GetChildren([out,  retval]  IEnumPerson **p);
    }
    因为IEum约定,允许潜在的集合可以用多种不同的形式来存储(例如:连接列表、数组
    、或者更为特殊的形式)。这个接口并不违反一个特定实现类的任何实现细节。
    如果需要与Visual Basic相兼容,那么结果集合可以用一个与自动化相兼容的集合接口
    或者一个SAFEARRAY接口指针来返回。前者看上去像这样:
    // enumerator interface to model collection of children
    [ uuid(30929829-5F86-11dl-B34E-0060975E6A6A),
       object, dual
    ]
    interface IPeople :  IDispatch {
       import "oaidl.idl";
       HRESULT Item([in]   VARIANT varIndex,
                      [out,  retval]  IPerson **ppRes);
       HRESULT Count ([out, retval]  long *pRes);
    // support VB for each syntax via an IEnumVARIANT
        [id(DISPID_NEWENUM)]
       HRESULT _NewEnum([out, retval] IUnknown **ppEnumVar);
    }
    [
       uuid (30929828-5F86-11d1-B34E-0060975E6A6A),
        object
    ]
    interface IPerson : IUnknown {
      import "oaidl.idl";
      HRESULT GetChildren([out, retval]  IPeople **ppRes);
    ...
    后者看上去如下:
    [
       uuid(30929828-5F86-11d1-B34E-0060975E6A6A),
       object
    ]
    interface IPerson : IUnknown {
       import "oaidl.idl";
       HRESULT GetChildren([out] SAFEARRAY(IPerson*)  *ppsa);
    使用IEumVARIANT的方法,对于很多Visual Basic程序员来说是很方便的。而在跨进程边
    界时,使用SAFEARRAY的方法,性能将会更好一些,这是因为它减少了往返(round-tri
    p)。然而,二者都存在着缺陷,即当把接口用VARIANT和SAFEARRAY来实现时,就会失去
    严格的类型检查。也就是说,客户必须为每一个元素发行一个额外的QueryInterface请
    求,以便获得IPerson的每一个对象的应用组件。
    另一个必须指出的,也是依然存在的事实是,这个接口的定义是假定所有的IPerson的实
    现是可雇佣的,也就是可以成为雇员。这样GetSalary和RaiseSalary的操作就是正确和
    有效的。因为,与结婚以及生育有关的操作是与是否有薪水的操作是毫不相关的(至少
    作者是这么认为的)。把两个接口分离开来是与COM的设计方法很相符的:
    [ uuid(30929828-5F86-11d1-B34E-0060975E6A6A), object ]
    interface IPerson : IUnknown {
       import  "unknwn.idl";
       HRESULT Marry([in] IPerson *pSpouse);
       HRESULT Reproduce(void);
       HRESULT GetSpouse([out, retval] IPerson **ppRes);
       HRESULT GetAge([ out, retval]  long *pRes);
       HRESULT GetChildren([out, retval] IEnumPerson **ppep);
      }
       [uuid(3092982A-5F86-11d1-B34E-0060975E6A6A), object ]
    interface ISalariedPerson : IPerson {
       import "unknwn.idl";
       HRESULT RaiseSalary([in]  long nAmount);
       HRESULT GetSalary([out, retval] long *pRes);
    }
    ISalariedPerson从IPerson派生的这个事实说明,所有salaries的组成部分都应与人有
    关。如果这个假设不成立(例如,如果动物也允许领取薪水),那么这个接口就应该从
    IUnkown派生出来,因为领取薪水与是否为人无关。
    你可能注意到,虽然我们已经耗费了大量篇幅但仍没有看到一个可执行的C++语句。这与
    COM的方式是一致的。接口(而不是对象)在COM设计方式里面占据着主要的地位。简单
    地把一些方法和数据成员堆砌在一起就不再工作了(对不起)。
    2  用分布式思想进行设计
    C++开发者通常容易犯的错误是,忘记了对象的作用域可以远超出它们出现的地方。考虑
    下面的C++代码片断:
    // rect.cpp
    int Rect::GetLeft()  throw()
        return m_nLeft;
    }
    // client.cpp
    void foo(Rect& rect) {
       cout << rect.GetLeft();
    }
    对于C++程序员而言,这是显而易见的调用GetLeft不可能失败。因此,注释函数不会返
    回异常。如果是一个有效的对象引用,那么除非用户在GetLeft正在执行的时候终止进程
    ,否则是不可能出错的。在这种情况之下调用代码(驻留在同一进程中),开发者很可
    能不知道其区别之所在。
    当设计一个可能驻留在不同主机,基于COM系统的对象时,不要过于自信。因为,网络可
    能不可靠,进程可能结束以及对象可能在没有任何警告之下突然消失。因此,在编写CO
    M代码时,必须牢记,在一般情况之下,被认为成功的方法调用,可能会因为在代码所控
    制范围之外的事件而失败。当用白板勾画设计时,当你在开发环境里编写代码时,处处
    抱有这种谨慎态度是非常有益的。
    为了允许客户检测通信问题,所有的方法都必须返回一个HRESULT。这使得COM的远程层
    用一个描述通信失败的错误码来代替对象的返回值。从理论上来说,为便于检查FACILI
    TY-PRC,通信错误可以通过使用定义在winerror.h里面的HRESULT_FACILITY宏来区分。
    然而,最近的COM库版本也开始使用FACIILITY_WIN32错误码。为了便于保持对映射HRES
    ULT到过时的异常机制的语法的支持,开发者可以使用[retval]来指明函数的逻辑返回值
    。在多数语言中,这种技术允许物理的HRESULT返回值自动地被译成异常,以便能编写出
    一些易读的客户代码:
    // rope.idl
    interface IRope  : public IUnknown {
      HRESULT GetLength([out,  retval]  long* pnLength)
    }
    // ropeclient.bas
    Sub foo(r As IRope)
      '  failed HRESULT will throw an exception
      ' which can be caught with an On Error statement
        On Error GoTo ExceptionHandler
        MsgBox "The rope is  " & r.GetLength()  &  "  ft long"
        Exit Sub
    ExceptionHandler:
        MsgBox "Invocation failure."
    End Sub
    与Java和Visual Basic开发者不一样,C++开发者必须手工检查每个HRESULT并且采取相
    应的措施。这种情况之下,通常所采用的方法是,生成一个封装类来把失败的HRESULT翻
    译成C++异常,这同时也允许开发者利用[retval]属性。Viual C++提供的#import命令就
    是这种技术的一个例子。
    另外一种普遍采用的方法是,使用一个C++类把失败的HRESULT映射到C++异常,如下所示

    struct HRX (
     HRX(void)  { }
     HRX(HRESULT hr)  { if  (FAILED(hr)) throw hr;  }
        HRX& operator=(HRESULT hr)
        {  if  (FAILED(hr))  throw hr;  return *this;  }
    };
    这个类使用很方便。这是因为它不依赖于特定编译器的扩展,而看上去更像一般的COM代
    码。
    void foo(IRope* pr)  {
      try  {
       HRX hrx;
       long     nLength;
       hrx = pr->GetLength(&nLength);
       char      sz[64];
       wsprintf(sz,  "The rope is %d feet long", nLength);
       MessageBox(0,  sz,  0,  0);
       }
        catch  (HRESULT hr)  (
            MessageBox(0, "Invocation failure.", " ",0);
        }
    }
    注意重载的operator=()函数映射失败的HRESULT到C++异常。
    到此为止,我们的讨论只是与过早消亡的对象有关。也有可能是,客户消失而没有及时
    的通报它所使用的对象。这实际说明,任何方法调用可能都成为最后的动作。虽然COM将
    最终释放所保持的所有对象引用,但对终止的客户不采取任何额外的动作。下面是一个
    没有考虑到这一事实的接口设计实例①:
    [ uuid(31CDF640-E91A-11dl-9277-006008026FEA), object]
    interface ISharedObject  :  IUnknown {
    //   call prior to calling DoWork
        HRESULT LockExclusive();
    //   ask object to do work while holding exclusive lock
        HRESULT DoWork();
    //   release lock held by LockExclusive
        HRESULT UnlockExclusive();
    }
    给定这个接口定义,一个预期的客户可能看上去像这样:
    // sharedclient.cpp
    void DoPrivateWork(ISharedObject *pso) {
        HRESULT hr = pso->LockExclusive();
        if (SUCCEEDED(hr)) {
            for (int i = 0; i < 10 && SUCCEEDED(hr);  i++)
                hr = pso->DoWork();
            HRESULT hr2 = pso->UnlockExclusive();
       }
    }
    如果客户进程在调用UnlockExclusive之前终止,那将会发生什么呢?假设对象在LockE
    xclusive实现中从操作系统获得了一个锁定,那么锁定将永远不会释放。可以确定的是
    ,客户的外部应用会被COM自动释放。但是,假设还有其他的外部客户,那么对象就不能
    检测到拥有这个锁定的对象已经消失了。
    客户过早死亡的问题可以通过COM的无用数据(单元)收集器来解决。既然COM在客户进
    程终止时将自动释放任何保留的对象应用,那么ISharedObject接口就可能基于接口指针
    释放时释放锁定,而不是显式的方法被调用时释放锁定。假设有以下接口的修正版:
    [ uuid(31CDF641-E91A-11dl-9277-006008026FEA), object]
    interface ISharedObject2  :  IUnknown {
    // call prior to calling DoWork and release  (*ppUnkLock)
    // to unlock object
        HRESULT LockExclusive([out]  IUnknown **ppUnkLock);
    // ask object to do work while holding exclusive lock
        HRESULT DoWork();
    }
    如果给定这个接口定义,那么对这个实现来说,创建第二个对象根据客户的最终释放来
    释放锁定就是轻而一举。为了适应代表获得了锁定的第二个对象本体的这种用法,客户
    应该修改为如下代码:
    //  sharedclient2.cpp
    void DoPrivateWork(ISharedObject2  *pso)  {
      IUnknown  *pUnkLock = 0;
    //  acquire lock
        HRESULT hr = pso->LockExclusive(&pUnkLock)
        if (SUCCEEDED (hr)) {
    //  do work 10 times while holding lock
         for  (iht i = 0;  i < 10 && SUCCEEDED(hr);  i++)
            hr = pso->DoWork( );
    //  release lock by releasing "lock"  object
            pUnkLock->Release ( );
        }
    }
    然后这个实现可以通过第二对象的析构函数来激发解锁操作。下面的代码片段使用Win3
    2的信号代替锁定来说明这种方法:
    // secondary object that releases lock at final release
    class UnlockCookie :public IUnknown {
      HANDLE m_hsem;
      ULONG  m_cRefs;
    public:
        UnlockCookie(HANDLE h)  : m_hsem(h), m_cRefs(0)  {}
        ~UnlockCookie()  {
        // release lock back to OS at final release
            ReleaseSemaphore(m_hsem,  1,  0);
       }
        // QueryInterface and AddRef omitted for brevity
        // Standard Release()  impl.  for heap-based object
        STDMETHDOIMP_(ULONG) Release()  {
            ULONG cRefs = InterlockedDecrement(&m_cRefs)
             if  (0 == cRefs)
                 delete this;
             return cRefs;
       }
    };
    // primary object that holds lock until released
    class SharedObject  : public ISharedObject2  {
       HANDLE m_hsem;  // init'ed in constructor
       STDMETHODIMP LockExclusive ( IUnknown **ppUnkLock)  {
           // acquire lock from OS
           WaitForSingleObject(m_hsem,  INFINITE);
           // create and return an intermediate object that
           // will unlock at its final release
           // memory allocation errors ignored for clarity
            ( *ppUnkLock = new UnlockCookie(m_hsem))->AddRef();
           return S_OK;
       }
         :        :        :
    };
    注意在这个例子中,如果客户在LockExclusive成功调用之后的任一时刻死亡,COM则将
    自动释放对中间对象的引用,而这将触发操作系统锁定的释放。
    虽然这里的接口设计不区分客户提前死亡和客户故意释放锁定,但是这很容易办到,通
    过下面细微的改变来定位这种差异:
    [ uuid(31CDF642-E91A-11dl-9277-006008026FEA), object]
    interface IUnlockCookie :  IUnknown {
    // call to release the underlying lock
       HRESULT UnlockExclusive();
    }
    [ uuid(31CDF643-E91A-11d1-9277-006008026FEA), object]
    interface ISharedObject3  : IUnknown {
    // call prior to calling DoWork and call
    // IUnlockCookie::UnlockExclusive to unlock object
       HRESULT LockExclusive([out]  IUnlockCookie **ppuc);
    // ask object to do work while holding exclusive lock
       HRESULT DoWork();
    }
    如果给定这个接口定义,那么对象现在就可以根据UnlockExclusive是否先于中间对象的
    最后释放调用来区分显式的解锁和提前死亡。
    附加的失败模式是开发者在转向分布式对象计算时唯一必须面对的问题。另外一个常见
    问题与执行方法调用时的潜伏时间(latency)存在有关。要考虑公共属性或特性的情况
    。当开发者首次构造一个面向对象的软件时,他们必须学习很多新的概念,这包括类和
    封装。为此,很多开发的新手在其职业生涯的早期都被告知,所有的数据成员都应该是
    私有的。为了尊重OO(面向对象)纯化论者的敏感性,很多开发者条件反射地使每个实例
    的数据私有化,然后着手定义各自的存取或转变(mutator)函数来把成员暴露于外部。
    这种技术,在一些派系中,用通俗的语言描述,就是"恰当封装"。但在很多情况之下,
    由于对外部世界暴露了详细的实现而丧失了很多潜在的好处。虽然这种技术或多或少仅
    比使用公有成员稍微好一些,但是这在C++领域还是可以接受的。因为编译器一般把这种
    函数用作内联函数,所以,通常情况下,成员访问对性能来说影响不大。
    当"恰当封装"在COM中使用时,真正的问题就产生了。在为一个分布式系统设计接口时,
    你必须不惜一切代价避免使用这种技术。由于独立编译以及COM固有的动态绑定的方法调
    用,内联实现是不可能的。更重要的是,每一个调用通常都意味着一个往返(round-tr
    ip)。考虑到调用者和被调用者的地点,这可能会导致一个远程调用(RPC)。
    作为一个接口设计者,你必须总是既要考虑到接口的性能又要考虑到接口的语义。一个
    需要由多个往返来实现的简单的逻辑操作的接口将导致用户性能不佳和潜在的条件竞争
    。例如,考虑以下的接口:
    interface IRect : IUnknown {
    HRESULT SetLeft       ([in] long nLeft);
    HRESULT SetTop        ([in] long nTop);
    HRESULT SetRinght    ([in] long nRinght);
    HRESULT SetBottom    ([in] long nBottom);
    HRESULT GetLeft       ([out,retval] long* pnLeft);
    HRESULT GetTop        ([out,retval] long* pnTop);
    HRESULT GetRight      ([out,retval] long* pnRight);
    HRESULT GetBonttom   ([out,retval] long* pnBottom);
    }
    这个接口需要四个往返来获得一个矩形的状态。虽然IRect从性能上来说显然不是优化的
    ,但是缺乏整体性(atomicity)是错综复杂、危害更大的问题。当你在忙于产生往返以
    获得整个矩形的状态时,另外一个客户可能正在你的后面改变状态。因为你不可能在一
    个往返中完整地获得整个矩形的状态,所以你不可能保证矩形在状态上的一致性。
    根据这种思想,你必须着手创建一个更好的矩形接口:
    interface IRect2: IUnknown {
    HRESULT SetRect( [in] long nLeft,
                           [in] long nTop,
                           [in] long nRight,
                           [in] long nBottom);
    HRESULT GetRect( [out] long* pnLeft,
                           [out] long* pnTop,
                           [out] long* pnRight,
                           [out] long* pnBottom);
    }
    这个新的,改进了的接口只需要一个往返来获得或设置状态。因此,这确保了能够真正
    获得一个一致性的矩形,并且在跨越边界方面性能也将得到很大提高。
    应当注意的是,IRect2已经失去了IRect的一些灵活性。假如你需要设置其中的一个坐标
    为100而其他的坐标不变,根据IRect2的当前设计,这是无法整体实现的,因为它要求两
    次往返。一个切实可行的解决方法是给SetRect函数增加一个额外的参数,来允许更细致
    的控制。
    HRESULT SetRect([in] long nLeft,
                       [in] long nTop,
                       [in] long nRight,
                       [in] long nBottom,
                       [in] DWORD grfWhichCoords );
    typedef enum tagSRWC {
      SRWC_LEFT    =0x0001,
      SRWC_TOP     =0x0002,
      SRWC_RIGHT   =0x0004,
      SRWC_BOTTOM =0x0008
    }SRWC; // SetRectWhichCoords
    另外一种方案是把IRect和IRect2的成员都汇总到一个单独的接口;然而,这种解决方案
    将产生一个很笨重的接口(像C++接口一样,COM接口应该完整和简练)。同时,这样的
    接口不允许一次恰好设置2个或3个属性来代替一次设置1个或4个属性。
    当你考虑到平移矩形的一些值时,会出现另外一个类似于IRect2接口的条件竞争。这是
    因为你必须用两个往返来完成一个单一的逻辑操作。在这种情况下,你可能考虑到矩形
    是很多需要平移的种类之一。因此,你需要通过为多种二维对象设计一种单独的接口来
    消除条件竞争:
    interface I2Dobject :IUnknown {
      HRESULT Translate ([in] long dx, [in] long dy);
      HRESULT Inflate ([in] long dx, [in] long dy);
      HRESULT Rotate([in] double degreesInRadians,
                        [in] long xCenter, [in] long yCenter)
    }
    你可能注意到了一个共同的主题。为了避免条件竞争,必须确保每一个逻辑操作只使用
    一个单独的往返。不必要的往返是非常有害的,应该避免使用。
    当设计接口时,使用一个数据包探测器来探测网络上传输具体的内容是一件很有趣的事
    ,这种探测可见是基于该方法、该对象或者是客户基础①。为了做到这一点,你在一个
    远程主机上必须有一个客户与对象通信。这是因为COM不使用TCP的回送来进行本地通信
    。一个特别有用的技术是观测一个列集参数的实际大小和结构,这个列集参数是任意给
    定的远程调用的列集参数。如果花上一个下午的时间使用netmon.exe(网络监视器)或ND
    R(网络数据)来监测协议细则,就会发现有些结构列集所需要的代价可能要比你预期的贵
    得多。
    有些开发者设想所设计的接口仅用于进程内(in-process)的客户,他们通常认为不必
    为网络失败、往返、网络表示以及条件竞争担心。但应该牢记为将来而设计这一原则。
    在这一点上COM是与其他技术一样的。必须意识到"进程内"和"进程外"只是一个暂时的实
    现细节,而很多优秀的接口设计在这两种情况之下都可以有效工作。实例9讨论了一个众
    所周知的、有关只在进程内使用的例子的陷阱。
    最后,记住基于接口的设计的优越性之一就是设计的重用。当花费大部分工作时间来察
    看有关成百上千个开发出来解决各种问题的新接口的文档时,你将会喜欢这种简单、设
    计优秀的接口(例如IUnknown)。一瞬之间,对于这些接口的众所周知的语义就一目了
    然了。当你学习一个通过COM来展示的新技术时,这种简单明了的接口将成为你最好的朋
    友。
    3  对象不应该有自己的用户接口
    模板-视控制器、文档-视、源-接受器,这些不同的方式表示的却是同一件事情--对象不
    应该有自己的用户接口(UI)。例如,设想一个应用程序在一个远程机器上创建了一个
    对象,假定这个对象可以计算圆周率(PI)精确到任意位数。同时假定对象要充分利用
    远程计算资源,而它却是在一台单独的机器上运行的,那么当客户机没有指明所需计算
    精度而请求计算开始时会发生什么情况呢?圆周率对象可能会(a)失败;(b)选择一
    些默认位数;(c)对客户应用程序进行回调调用来请求位数;(d)不停地计算直到请
    求计算中止为止;(e)询问最终用户要计算多少位。如果你选择e以外的情况,那么你
    就答对了。
    反之,如果对象产生一个请求精确的位数的对话框,谁又能看到呢?用户不是在运行圆
    周率计算程序的机器上运行客户程序的。在很多情况下,两台机器是在不同房间。如果
    用户比较幸运的话,有可能系统管理员正好在远程机器上工作,而对象又正好在一个可
    以询问计算位数的环境中运行,在这种情况之下,有些管理员就会猜测出正确的位数,
    然后替用户输入。然而,如果用户不那么幸运,那么对话框将静静地等待着输入,而计
    算何时完成就难以想像了。
    在这个远程实例中,对于从对象实现中削减用户接口的观点我们是很难反驳的。显而易
    见,预定的用户不可能看到在远程机器上运行的对象中构造的用户接口的。那么如果在
    同样的机器,甚至于同一个地址空间会怎样呢?拥有用户接口对象就可行?不行,理由
    如下:对象的用户接口的外观是怎样的?是独立的还是在自己的窗口中运行?是不是一
    个更大的窗口体系的一部分?是否应该为白底黑字?是否应该有二维外观或者更现代的
    三维外观?谁知道呢?对象的用户(客户)当然不愿意最终用户认为圆周率的计算引擎
    是应用程序的不同部分,是从外面硬加进来的,而希望看到的是一个整体。不管客户在
    任何特定时候使用的用户接口的形式如何,pi计算引擎的用户接口都应与客户的用户接
    口无缝地结合起来。一个可行的解决方法是,为控制每一个可想像的显示选项和设置增
    加一个用户接口。然而,这可能与你本来的功能核心无关,因为你不可能预计到将与你
    的对象集成的各种用户接口是怎样的,所以这种方法只能适用于简单的情况。一个更好的
    方法是让对象做实际的工作,然后把用户接口看成一种粘合剂,它允许客户开发者使用
    一种标准的、可任意使用的机制,以便用户管理自己的对象。把你的底层组件与用户接
    口隔离开来是很重要的,否则的话,每次用户接口的变化将使所有的内容受影响。而且
    用户接口变化频率是最大的,特别是在开发的最终阶段。
    这并不是说一些对象不应该使用用户接口对象。很多用户接口框架进一步将规划布局分
    离成为独立的部分,以便可以集成成一体。在COM中,专有用户接口对象被称为控制。一
    个控制主要拥有自己绘出的那一部分窗口。既然控制的存在仅仅代表了另一个对象的用
    户接口,那么就不应该维护那些不被本地用户接口所需要的状态。任何独立于用户接口
    的状态(以及功能)必须驻留在被控制所代表的实际的对象中。这使得多控制的用户能
    够用不同的方式来处理并显示同一个对象。即模板-视-控制。
    只是简单地创建分离的用户接口对象是完全不够的。开发者必须确保任何重要逻辑以及
    行为是驻留在用户接口对象层以外,而且是驻留在对象的逻辑以及抽象对象层。一个MF
    C或Visual Basic开发者普遍具有的坏习惯是将菜单项事件处理器作为所有程序的逻辑中
    心。当开发者改变用户接口时,就会发现不得不修改逻辑代码来匹配用户接口。这就使
    得替换整个用户接口框架变得尤为困难(例如MFC),因为应用程序的逻辑与具体的框架
    代码是紧密结合在一起的。
    这种问题通常是通过改进文档-视的结构模式到它的极端形式:多层结构而得到解决。
    在一个多层结构中,增加每一个抽象层,虽然会增加通信负载,但也提供了获得可扩展
    性、可维护性、可伸缩性以及灵活性的机会。一个关于多层结构的天然杰作是,核心对
    象层与最后的用户接口分离。这是面向对象的基本承诺:每个对象并不是自给自足的,
    而是通过对象紧密地工作在一起,来完成所需完成的任务。当任务变化时,同样的对象
    可以在不同的方式下重用而不需完全重建。
    4  注意COM的单实例
    通常人们把大量的时间和精力花费在实现COM的单实例上。单实例就是它的类的一个,并
    且是唯一的对象。模型运动使得单实例的概念正式化为一种技术,它允许多个客户通过
    一个众所周知的进入点获得对同一对象的引用。
    单实例通常用于提供基于对象的可替代类一级方法(例如C++类的静态成员函数)的集合
    点。最直接地把这种技术应用到COM,就是从类对象暴露一个可定制的接口。然而,这要
    求脱离大多数COM开发工具(例如ATL,MFC)的一些默认行为。因此,这不像想像中那样
    成为一种普遍的实践。相反,多数开发者选择重载IclassFactory::CreateInstance的实
    现来达到同样的效果。
    class Dog :public IDog {
    //implementation of Dog deleted for clarity
    };
    //singleton version of CreateInstance
    STDMETHODIMP DogClass::CreateInstance(
                   IUnknown *pUnkOuter, REFIID riid, void **ppv)
    {
    //declare a "singleton" object
     static Dog s_Dog;
    //disallow aggregation
     if (pUnkOuter)
      return (*ppv = 0), CLASS_E_NOAGGREGATION;
    //return a pointer to the "singleton"
     return s_Dog.QreryInterface(riid,ppv);
    }
    当在类定义中使用DECLARE_CLASSFACTORY_SINGLETON宏时,ATL(ActiveX模板库)使用
    了这种技术的一个变种。从技术的角度来说,这符合前面单实例的定义:每个客户通过
    一个众所周知的进入点--Dog类的对象获得的同一实例的Dog对象的引用。然而,这种方
    法也存在着问题。
    首先,如果通过CreateInstance这种小技巧来暴露(expose)单实例,那就违反了ICla
    ssFactory的语义。CreateInstance在文档中被解释为创建一个新的未初始化的对象。希
    望两次调用CreateInstance产生两个独立的对象的客户,将会由于这种不正常的实现而
    感到非常惊奇。
    从语义上来说,通过仅使用一些其他的接口来获得对单实例的访问将会更好一些。为了
    达到这个目的,你可以定义自己的客户接口,或者可以重用标准接口来匹配语义需求(
    例如IOleItemContainer)。
    // Custom interface implemented by class object
    interface ISingletonFactory : IUnknown {
     HRESULT GetSingleInstance([in] REFIID riid,
               [out, iid_is(riid)] void **ppv);
    }
    // Implementation of GetSingleInstance
    HRESULT DogClass :: GetSingleIeInstance(REFIID riid,
                                    void **ppv)
    {
     static Dog s_Dog;
     return s_Dog.QueryInterface(riid, ppv);
    }
    应当注意的是,这种技术在那些隐藏类对象细节的语言中是难以实现和使用的(例如,
    Visual Basic、Java以及各种脚本语言)。关于这个主题,开发者可以实现一个变种,
    通过它使用一个单独的Manager类,利用它的实例来实现一个在各种语言下都能工作和访
    问的单实例。
    你可能会担心这样会使CreateInstance的语义松散,当然你也可能不会担心。但不管怎
    样,这里有另外一个问题必须关注。如果你的CreateInstance(GetSingleInstance或等
    价函数)的实现总是返回对同样COM对象的引用,那么这就可能违反了COM并行性的规则
    。COM对象是运行在一个套间里面的,如果希望跨套间边界地移动接口指针,那么COM就
    要求对接口指针进行列集(见单元28)。如果你的单实例在一个单独进程中可被用于多
    个套间,并且类对象直接返回这个接口指针,那么你的代码就违反了这个规则。这意味
    着如果你的类被当成进程内服务器来使用,那么你必须标记它为ThredingModel=Free,
    或者在注册表中空着ThreadingModel属性。否则,你就将承受与使用自由线程列集(FT
    M)的对象一样的缺陷(至于为什么套间-中立(apartment-neutral)的对象需要特别
    注意以及适当实现的详细描述,请见例32)。
    如果你是一个COM的坚决拥护者,并且掌握了有关COM的所有线程细节。那现在你开始考
    虑设计吧。首先最重要的是,这个单实例模式假设状态和行为之间有一种自然的联系。
    即使是在COM范围以外,两个或两个以上的对象共用一个状态也是很平常的。通常情况下
    ,开发者在COM中使用单实例习惯语法来为两个或两个以上客户进入公共状态获得一个集
    合点。如果这就是全部所需,那么下面的单实例实现
    class Dog {
      long m_nHairs;
      HRESULT Shed(void) {
         m_nHairs -=100;
         return S_OK;
      }
    };
    可以被替换为:
    class Dog {
      static long s_nHairs; // shared state
      Dog(void) : m_nHairs(s_nHairs) {}
      long& m_nHairs; // note that a reference is now used
      HRESULT Shed(void) {
         m_nHairs -= 100;
         return S_OK;
      }
    };
    在前一种情形下,开发者需要一些技巧来确保只创建Dog的一个实例。而在后一种情况下
    ,所创建的Dog的数目是没有限制的。这是因为每一个Dog使用一个静态变量来共享hair
    数目的。后一方法的优越性在于它通过实现可以在构造函数中不易被人察觉地改变状态
    的管理策略,来实现每个客户的行为。这在一个基于单实例的普通的方法中是不可能的
    ,因为每个客户是从一个特定的对象中获得引用的。此外,后一方法在任一语言中都是
    易于实现的,而不用考虑这种语言是否对COM底层支持。
    要重点记住的是,单实例解决方案最初是为修正一个经常在独立的程序中出现的问题而
    构想出来的。那么在一个包含多台机器、多个进程的分布式程序中,单实例到底扮演的
    是什么角色呢?
    首先,什么是单实例的范围?一个COM对象可以在客户的进程中、在一个客户机器上的独
    立进程中、以及在其他机器的一个进程中被激活。如果通过实现类对象来返回一个单实
    例的引用,那么是否那个单实例在进程中、在机器中、或者在一个跨跃多个机器的网络
    中是唯一的呢?
    用于分布式计算时,由于单实例限制了负载平衡、并行性管理、优先权以及每个客户的
    状态管理,它相当迅速地失灵了。假设在一个航班订票系统中应用单实例。如果你想代
    表一个特定的航班,假设是从波士顿的Logan机场到LAX的联合航班162,作为一个网络范
    围的单实例,那么你就创造了一个巨大的瓶颈。成千上万的来自世界各地的用户需要同
    时处理一个对象,而这不是一个单独的COM对象所能负担的。单实例机制根本就不是为这
    种问题而设计考虑的。
    那么解决方案是什么呢?要意识到,单实例是建立在一个共享的物理本体的概念之上,
    换句话说,一个单实例暗示着,在单机、单进程的单套间中,有一个单COM本体体,它是
    你的分布系统中某个本体的、也是惟一的代表。放弃这种想法而去考虑一下逻辑本体。
    允许多个COM对象在多台机器中的独立进程中的不同套间中代表同一个逻辑本体。为了获
    得单实例的效果,所有这些对象,一一对应着每个客户,仅访问同一个共享状态。
    如果沿着这条路走下去,那么你就打开了负载平衡之门。如果你为每个客户创建一个独
    立的COM对象,那么你同样可以在每个对象中缓存下每个客户的状态。然后你就可以根据
    由COM垃圾收集器释放的单个客户对象检测到单个客户的死亡(这种技术的例子参见实例
    2)。假设每一个对象都引用共享状态,从技术上来说你是把并行性问题降低了一个层次
    ,这意味着你(不是COM)处于保护并行访问的境地。事务处理,对这种问题来说是具有
    巨大帮助的(实际上,基本上是MTS)。
    那么我们结论是什么?如果你是单实例的狂热支持者,只要你理解单实例在什么地方真
    正有帮助作用,在什么地方真正起阻碍作用,那就会游刃有余。当建设的分布式系统越
    来越大时,应该意识到网络范围的单实例用途将越来越小。同时必须小心的是,如果计
    划使用MTS作为基本结构,单实例是禁止使用的。
    5  不要允许C++的异常跨越方法边界
    异常,在C++中提供了一种可被用于跨越由多个公司所建立的类库的标准的、可扩展的错
    误处理机制。不幸的是,因为对于C++异常来说没有二进制标准,所以C++异常的使用要
    求所有的应用程序以及库的源代码要一起编译成一个单独的可执行映像文件。这破坏了
    使用COM的一个核心目的。因此,仅在独立的可执行映像文件之间抛出异常是无法正常工
    作的。这对于一个COM开发者来说,意味着从COM的方法调用中无法抛出C++异常。
    当从基于纯C++的开发转到COM开发时,开发者必须意识到的一个问题是C++异常的语法是
    由两个方面组成的:一方面,异常仅是一个函数的隐含的输出参数;另一方面,异常用
    于终止正常的执行线程并且强制调用者对异常做出反应。对于前者的处理来说那是小菜
    一碟,仅通过增加额外的[out]参数就可以传输在执行方法期间可能发生的异常事件的描
    述信息。而对于后者的处理,可能有不同的意见,即在客户程序中要指定可选择的执行
    路径应该不是对象的责任。这自然是COM设计者的基本原则,即认为怎么处理应用程序级
    的错误应该由客户自己决定(毕竟,并不是所有的语言都支持异常处理)。也正是由于
    这个原因,COM对于C++式的异常没有提供显式的支持。
    因为对于COM方法来说抛出异常是不合理的,所以在使用抛出C++异常的库时,你必须把
    所有的COM方法实现用花括号包含在一个try-catch块中。很显然,为了模拟C++异常的
    表达方式,可能要使用非常具体的甚至于自定义的错误代码:
    HRESULT CoPenguin::Fly() {
       try {
        return TryToFly();
       }
       // Handle standard c++ out of memory exception
       catch (xalloc& x) {
           return E_OUTOFMEMORY;
    }
       // Handle custom errors (published in the IDL)
       catch (xbiology& x) {
         return BIRD_E_CHEATEDBYNATURE;
       }
       // Handle everything else
       catch (…) {
         return E_FAIL:
        }
    }
    为了定义一个自定义HRESULT,可使用winerror.h中的标准的MAKE_HRESULT宏来定义。
    #define MAKE_HRESULT(sev,fac,code) \
      ((HRESULT) (((unsigned long)(sev)<<31) | \
      ((unsigned long) (fac)<<16) | \
      ((unsigned long)(code))) )
    简而言之,一个HRESULT被分成一个重要程度位,一个设备代码以及一个错误码。重要程
    度位是用于描述成功或失败的,设备码用于定义错误码的范围,而错误码本身用来描述
    到底发生了什么。自定义的HRESULT使用FACILITY_ITF设备。不幸的是,微软的OLE小组
    在接口设备中为特定的OLE和通用的COM结果抢占了前面的512个码。当定义一个与接口有
    关的HRESULT时,开发者必须记住要从0x1ff开始往后,就像以下代码所示:
    #include <winerror.h>
    import "objidl.idl";
    [uuid(18FDEA81-1115-11d2-A4B3-006008D1A534), object ]
    interface IBird : IUnknown {
      enum BirdResults {
        BIRD_E_CHEATEDBYNATURE =
           MAKE_HRESULT(SEVERITY_ERROR,
                            FACILITY_ITF,
                            0x200),
           BIRD_S_FLYINGHIGH =
             MAKE_HRESULT(SEVERITY_SUCCESS,
                            FACILITY_ITF,
                            0x201)
       };
    //IBird methods…
    }
    可惜的是,一个自定义的HRESULT不会捕获到可能在传统C++异常中用到的表示方式。例
    如,一个HRESULT不会传输错误源或错误的文本描述。然而,开发者可以使用FormatMes
    sage函数将一个标准的HRESULT翻译成文本描述信息以便其适合调试。代码如下所示:
    void OutputDebugHresult(HRESULT hr ) {
      char szDesc[1024];
      BOOL bRet = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM,
                            0, hr, 0, szDesc, sizeof(szDesc), 0);
      if (!bRet)
        1strcpyA(szDesc, "Unknown Error");
      OutputDebugString(szDesc);
    }
    很难(但并非不可能)把这种方法扩展到用于获得自定义HRESULT的描述。幸运的是,有
    一个更简单的选择:COM错误信息对象(通常叫作COM异常)。
    每一个使用COM的线程都有一个隐含的错误信息对象引用,通过COM与之关联。在线程的
    初始化阶段,这个引用为空(NULL),表示这里无异常信息。当一个对象希望给调用者
    抛出一个异常时,它将把一个错误信息对象与当前的线程相关联,然后指示失败的方法并
    返回一个SEVERITY_ERROR  HRESULT。如果客户希望捕获异常那么仅需要检查引用是否被
    设置就行了。
    COM错误信息对象是至少实现了IErrorInfo接口的COM对象。
    interface IErrorInfo :IUnknown {
      HRESULT GetGUID ([out] GUID * pGUID);
      HRESULT GetSource([out] BSTR * pBstrSource);
      HRESULT GetDescription ([out] BSTR * pBstrDesc);
      HRESULT GetHelpFile([out] BSTR * pBstrHelpFile);
      HRESULT GetHelpContext([out] DWORD * pdwHelpContext);
    }
    IErroInfo不仅提供了一个错误的文本描述和错误源,而且还提供产生错误方法的接口标
    识符。更可喜的是,它还可以提供一个帮助文件的引用,以允许开发环境为开发者提供
    大量的关于这个对象的错误信息。
    当在COM方法调用过程中发生错误,错误信息对象可以被构造并且甚至于跨套间边界返回
    。错误信息对象接口与HRESULT就会一起被列集回调用者①,然后可被任何COM客户访问
    。COM在这种方式之下提供抛出和捕获独立于语言的异常机制。
    虽然可以通过实现自己的错误对象来实现IErrorInfo以及任意所期望的自定义接口,但
    是大多数客户只是查询IErrorInfo接口。这是因为很少有客户支持IErrorInfo以外的接
    口,他们通常使用COM提供的GreateErrorInfo函数就感到非常方便。该函数创建一个默
    认的错误信息对象,然后返回一个用于设置状态的ICreateErrorInfo接口:
    interface ICreateErrorInfo : IUnknown {
    HRESULT SetGUID([in] REFGUID rguid);
    HRESULT SetSource([in] LPOLESTR szSource);
    HRESULT SetDescription([in] LPOLESTR szDesc);
    HRESULT SetHelpFile([in] LPOLESTR szHelpFile);
    HRESULT SwtHelpContext([in] DWORD dwHelpContext);
    }
    一旦错误信息对象的状态被设置,就可以使用SetErrorInfo函数来抛出异常。下面就是
    一个例子:
    HRESULT ComThrow(LPCOLESTR pszSource, LPCOLESTR pszDesc,
                        REFIID riid) {
      ICreateErrorInfo* pcei = 0;
      HRESULT hr = CreateErrorInfo(&pcei);
      if (SUCCEEDED (hr)) {
    pcei->SetSource(const_cast<OLECHAR*>(pszSource));
    pcei->SetDescription(const_cast<OLECHAR*>(pszDesc));
    pcei->SetGUID(riid);
    IErrorInfo* pei = 0;
    hr = pcei->QueryInterface(IID_IErrorInfo,
                                  (void**)&pei);
    if (SUCCEEDED(hr)) {
       hr = SetErrorInfo(0, pei);
       pei->Release();
    }
    pcei->Release();
    }
    return hr;
    }
    在客户端,可以使用GetErroInfo函数在COM方法调用失败以后获得错误信息对象:
    void ComCatch() {
      HRESULT      hr;
      IErrorInfo* pei = 0;
      hr = GetErroInfo(0, &pei);
      if (hr == S_OK) {
    BSTR bstrSource = 0;
    BSTR bstrDesc = 0;
    pei->GetSource(&bstrSource);
    pei->GetDescription(&bstrDesc);
    OLECHAR szErr[1024];
    wsprintfW(szErr, L"%s: %s", bstrSource, bstrDesc);
    outputDebugStringW(szErr);
    SysFreeString(bstrSource);
    SysFreeString(bstrDesc);
      }
    }
    当使用错误信息对象时,对象必须实现ISupportErrorInfo接口(这与C++抛出异常规范
    是一致的)。能够顺利实现的(well-implemented)客户将为这个接口查询一个对象以
    及调用一个单一的方法,InterfaceSupportsErroInfo,来确保不必要的异常的传播。
    至今为止所有的讨论都忽略了这一事实,即C++异常是类型化的,允许在一个单一的cat
    ch块中分层次地捕获。如果这种能力对设计来说非常重要,那么可以通过映射C++类型化
    异常作为COM输出参数来获得这种类型异常捕获机制。这就意味着以下的C++函数定义:

    short LibGetMyShort(void) throw (long, std::wstring);
    在IDL中看上去像这样:
    [
      uuid (18FDEA80-1115-11d2-A4B3-006008D1A534),
      object, pointer_default(unique)
    ]
    interface IFakeExceptions : IUnknown {
      HRESULT GetMyShort([out] long **ppl,
                           [out] LPOLESTR *ppsz,
                           [out, retval] short *pres);
    }
    如果给定这个方法定义,那么对象就能够提供以下C++异常到[out]参数的映射:
    STDMETHODIMP GetMyShort(long **ppl, LPOLESTR *ppsz,
                               short *pres) {
      try {
    //null out exception parameters
       *ppl = 0;
       *ppsz = 0;
    //call exception-throwing C++ code
       *pres = LibGetMyShort();
       return S_OK;
     }
    catch (long 1) { // map exception to [out] param
      *ppl = (long*) CoTaskMemAlloc(sizeof(long));
      **ppl = 1;
     }
     catch (const wstring& s) {//map exception to param
       DWORD cb = (s.length() + 1) * sizeof(OLECHAR);
       *ppsz = (OLECHAR*)CoTaskMemAlloc(cb);
       wcscpy(*ppsz, s.c_str());
     }
     catch (...) {
     }
     return E_FAIL;
    }
    如果客户希望显式地把[out]参数映射回C++异常,可以这样做:
    short CallGetMyShort(IFakeExceptions *pfe)
                               throw (long, std::wstring) {
      long *pl; LPOLESTR pwsz = 0; short s;
      HRESULT hr = pfe->GetMyShort(&pl, &pwsz, &s);
      if (SUCCEEDED(hr))
      return s;
      if (pl) { // a long was thrown
      assert(pwsz == 0); // only one exception allowed
      long ex = *pl;
      CoTaskMemFree(pl);
      throw ex;
     }
     else if (pwsz) { // a string was thrown
       assert(pl == 0); // only one  exception allowed
       std::wstring ex = pwsz;
       CoTaskMemFree (pwsz);
       throw ex;
     }
     else
       unexpected(); // std routine for bad exceptions
    }
    可以确定的是,这种技术需要大量从显式参数到异常的手工转换,但是结果保留了原先
    在使用类型化异常中可用的语义信息。然而,因为用这种方式映射C++异常相当麻烦,所
    以很多开发者使用简单(虽然功能少)GetErrorInfo/SetErrorInfo机制。
  • 相关阅读:
    逆元模板
    同余方程
    计算系数
    Mayan游戏
    【分治】聪明的质检员(二分)
    瑞士轮(归并排序)
    极值问题
    传纸条
    2014-2015-1学期学习计划
    桌面综合实训答辩验收详情
  • 原文地址:https://www.cnblogs.com/chengxin1982/p/1875484.html
Copyright © 2011-2022 走看看