65.9K
CodeProject 正在变化。 阅读更多。
Home

复合-访问者模式:面向对象的实现方式

2004 年 8 月 16 日

CPOL

13分钟阅读

viewsIcon

51536

downloadIcon

388

实现了 Composite-Visitor 模式,避免使用递归的泛型代码。

引言

本文是“比较编码方法”系列的自然演进,并且可以是对 Dave_H 关于同一主题的相应文章的回应。

免责声明(或……一些看法)

本文无意参与宗教辩论:我已经(希望)解释过,我对此类宗教纠纷不感兴趣。我感兴趣的是评估可能的替代方案。

Dave 提出的解决方案非常出色且优雅,完全基于泛型编程技术,但需要一些 RTTI 特性来处理多态。我也使用过这些技术,主要是在我之前创建 W32 引用计数包装器 的代码中。在这里,我只想展示另一面,主要使用 RTTI 和 OOP,并带有一些模板修饰。何时使用一种或另一种……主要取决于您部署的应用的需求,以及……为什么不,您个人的看法。

所有这些“基于模板”的解决方案的共同点是所谓的“奇特的递归模板模式”:模板类以一个假定从模板类自身派生的类作为模板参数。这允许基类通过将基类“this”转换为 derived* 来调用假定在派生类中实现的函数。有关所有示例,请参阅引用的文章。简而言之

template <class TDerived>
class CBase
{
    void basefn()
    {   static_cast<TDerived*>(this)->derivedfn();    }
};

class CDerived: public CBase<CDerived>
{
    void derivedfn() { /* do something specific here */ }
};

另一种方法是使用模板派生而不是模板基类,就像这篇其他文章一样。简而言之

class CBase
{
    void basefunction() { /* do something specific */ }
};

template<class TBase>
class CDecorator: public TBase
{
    void decorate() { basefunction(); }
};

这两种方法的限制是无法处理运行时多态:不同类型的对象在同一数据结构中协作。

这种情况(如引用的文章中所述)必然需要 RTTI 和 dynamic_cast

但关键点来了:假定您无法摆脱 V 表和 RTTI 描述符,您是否仍然需要 CRTP?

毕竟,它允许从基类调用派生类中的函数。但是……这可以通过虚函数来完成。

因此,我在这里提出一种方法来实现该范例,主要使用泛型代码,大部分代码使用传统的 OOP。

结果是编译器可以翻译成一系列类的实现。而不是每次新类需要时都重新注入代码。

这使我能够将实现代码(放在库中)与应用程序代码(放在各自的项目中)分开。由链接器将(各种)OBJs 绑定到 EXE,而不是编译器将所有代码生成到一个(或几个)OBJ 中。

主要设计规则

有些人认为多重继承是疯狂的,但认为带有许多参数的模板是“好东西”。我看不到两者之间有任何特别的区别:它们是完全对偶的。因此,我将使用多重继承和多参数模板,主要参考“比较”文章中描述的“多重部分实现”方法。

总体方案

在此实现中,我将遵循此总体方案

General Scheme

我不想担心继承、虚基类或多重继承,以及模板注入的真正或假定的低效率。我只想拥有一个可以独立翻译的代码,并利用所有可用功能来保持设计足够优雅和模块化。性能不是“关键因素”,尽管我也会注意这一点。

对象所有权

由于我们将实现多态对象的集合,因此我们需要以“指向共同基类的指针”来引用它们。这就带来了“谁拥有该对象?”的老问题(即:谁有权销毁它?)

尽管它不是一个万能的解决方案,但引用计数是一个不错的折衷方案。然而,在这种情况下,我不会深入研究多少复杂的策略驱动的智能指针,而是通过“模板和接口”的混合。

Composite - Visitor

Composite 是包含其他对象的对象。它们的自然聚合是树,但不是一种 `std_like::tree<T>`(其中 `T` 元素不知道自己是树的一部分),而是由树节点组成的树,其中对象本身就是节点,并且是多态的。

Visitor 是一种对象迭代器,能够遍历结构并解引用其中包含的对象。

数据转换器

如果结构是根据指向共同基类的指针构建的,那么访问派生类中的对象组件需要从共同基类进行 dynamic_cast 到所需的组件。这段(二进制)代码显然因组件而异,所以这是一个模板具有真正优势的好地方:它们可以从通用源生成代码。

集合

集合是同类类型的容器,它们的活动不具体依赖于所托管的类型,并且所托管的对象对它们被收集的事实一无所知。因此,集合可以有一个共同的接口,但它们不能有一个共同的二进制代码,因为这段代码依赖于托管对象的大小。所以,模板作为该接口的实现确实非常有效。就此而言,STL 集合在 OOP 应用中也扮演着其应有的角色!

相反,多态集合可以实现为指向共同基类的同类指针的集合。这个考虑可以引出同类集合作为多态集合“偶然”填充了相同类型对象的特例的想法。但有一种情况不能这样做:数组。数组要求同类对象存储在连续的内存中。(也就是说,“this+1”必须是下一个对象,而这不能通过存储指针来实现。)而这在某种程度上使得模板总是更适合这些任务。

现在所有论点都已介绍完毕,让我们来描述提出的实现。

数据转换器和可转换的 Visitor

由于所有 Composite 都将存在于堆上,一个非常基本的数据转换器将是一个具有指针语义的小对象,它注入动态类型转换并通过装饰(继承)一个“可转换”的类来执行适当的赋值。

struct Ptr,因此,是一个用于通用 TBase 的“模板装饰器”,它可以被 Ptr 继承,为其提供一些函数

    template<class T, class TBase> //T dervied from TBase

    struct Ptr: public TBase
    {
        //TBase must provide:

        // typedef xxx _TRefType //defines the interface it visit

        //  void _assign(_TRefType* p) //required for assignment

        //  _TRefType* _access() const //required for asignment and dereference

        // void _inc(); //required for ++ operator

        // void _dec(); //required for -- operator

        // _TRefType* _subscript(int); //required for [] operator


        typedef typename TBase::_TRefType _TRefBaseType;
        typedef typename T _TPtrType;
        
        ...
    };

Ptr 实现所有赋值、复制、转换、下标、解引用运算符(所有可以接受指针作为参数的经典运算符),通过 dynamic_cast 机制自动将 _TBaseRefType 转换为 _Type

特别是

private:
    friend struct Ptr;
    T* _pCached;
public:
    typedef typename TBase::_TRefType _TRefBaseType;
    typedef typename T _TPtrType;
    struct exception: public XException 
    {}; //the exception of this operations

    
_TPtrType* _convert() const { return dynamic_cast<_TPtrType*>(_access()); }
Ptr() {_pCached = NULL;}
Ptr(const Ptr& p) { _assign(p._access()); _pCached = p._pCached; }
template<class A, class B>
    Ptr(const Ptr<A,B>& p) { _assign(p._access()); _pCached = _convert(); }
Ptr(_TRefBaseType* p) { _assign(p);  _pCached = _convert();}
Ptr& operator=(const Ptr& p) 
   { _assign(p._access()); _pCached = p._pCached; return *this; }
Ptr& operator=(_TRefBaseType* p) 
   { _assign(p); _pCached = _convert(); return *this; }
template<class A, class B>
   Ptr& operator=(const Ptr<A,B>& p) 
   { _assign(p._access()); _pCached = _convert(); return *this;}

operator _TPtrType*() const { return _pCached; }
_TPtrType* operator->() const 
    {if(!_pCached) ThrowExcp<exception>(); return _pCached; }
_TPtrType& operator*() const 
   {if(!_pCached) ThrowExcp<exception>(); return *_pCached; }
bool operator!() const { return !_pCached;  } //no convertible object available

bool operator~() const { return !_access(); } //no object available

void New() { _assign(new T); _pCached = _convert(); }
template<class I> void New(const I& i) 
   { _assign(new T(i)); _pCached = _convert();}
Ptr& operator++() { _inc(); _pCached = _convert(); return *this; }
Ptr& operator--() { _dec(); _pCached = _convert(); return *this; }
Ptr operator++(int) { Ptr r(*this); _inc(); _pCached = _convert(); return r; }
Ptr operator--(int) { Ptr r(*this); _dec(); _pCached = _convert(); return r; }
_TPtrType& operator[](int i) const 
    { _TPtrType* p = _convert(_subscript(i));
if(!p) ThrowExcp<exception>(); return *p; }

实际上,一个纯粹的“装饰器”,引用了之前的方案。注意 `_convert` 是从基类提供的类型到 Ptr 操作的类型(假定是基类类型派生而来)的动态转换。

Ptr 的实现包含一个私有的 T* _pChache 值,并在赋值或复制期间执行类型转换。所有其他“读取”运算符(如 `*`、`->` 等)都返回缓存的值。这有助于限制正常执行中进行的转换次数。

另外请注意,尽管 Ptr 是一个模板,但其定义中没有使用递归。

可引用计数对象

以完美的 OOP 风格,让我们从一个interface 开始。

首先:对我而言,interface 仅仅是 struct 的一个别名。因此,该关键字的使用纯粹是建议性的。虽然完全为空,但它有一个 v-table,所以最好为它们提供一个虚析构函数。它什么都不做……但它是虚的。如果使用接口指针调用 delete,这是正确工作的必需品!

可引用计数对象可以实现 IReferable,为其中立函数提供含义

    interface IReferable
    {
    private:
        virtual void AddRef()=0;
        virtual void Release()=0; 
        virtual void Delete()=0;
    protected:
        virtual ~IReferable() {}
    }

它们都是私有的,因为除了明确声明为 friend 的类之外,任何人都不能直接调用它们。

为了有一个指向它的智能指针,我们需要一个“可转换的 Visitor”,它将 `_assign` 转换为 `Addref` 和 `Release`。

由于这是 IReferable 特有的,我将其定义为 Ireferable::XPtr(内部 struct),并使其成为 IReferable 的 friend(因此它可以调用 IReferable 函数的重写)。

    struct XPtr //smart pointer to a referable: a TBase for XPtr<TTBASE,>

    {
    protected:
        void _assign(IReferable* pR);
        IReferable* _access() const;
    public:
        typedef IReferable _TRefType;
        XPtr();
        ~XPtr();
    private:
        IReferable* _pRef;
    };

其中 `_assign` 被编码为……

    void IReferable::XPtr::_assign(IReferable* pR)
    {   
        if(pR) pR->AddRef();
        if(_pRef) _pRef->Release();
        _pRef = pR;
    }

由于我们讨论的是一个独立的​​对象,因此实现 `_inc`、`_dec` 和 `_subscript` 没有意义。

请注意,为了确保在 `pR == _pRef` 的情况下,指向的对象在 Release 之后不会被销毁,必须在调用 Release 之前调用 AddRef

指向 IReferable 的智能指针可以这样获得:typedef Ptr<IReferable, IReferable::XPtr> PReferable

通常,给定一个继承自 IReferableCSomethingReferable,可以定义一个 Ptr<CSomethingReferable, IReferable::XPtr>,其 `*` 和 `->` 运算符将自动将持有的对象转换为 CSomethingReferable

IReferable 实现:EReferable

为了避免在每个对象上重新实现接口,可引用对象可以继承一个已实现的 IReferable,例如 EReferable

它处理引用计数器,并通过增加和减少它来实现 AddRefRelease

由于我们无法知道复合对象的继承图可能有多复杂,但由于此接口每个实例只需要一次,因此需要虚继承。

AddRefRelease 也通过调用其他虚拟“钩子”函数来实现,这些函数声明为“无操作”,但可以在继承的对象中重写:OnAddRefOnFirstRefOnReleaseOnLastRelease

现在,可以通过虚继承 EReferable 并将其指针定义为 Ptr<yourclass, yourclass::XPtr> 来定义一个可引用的类。

EReferable 的通用装饰器:CReferable<U>

对于不能继承自 EReferable 的类或简单类型,CReferable 通过继承 EReferable 来实现,它包含一个 U 成员,提供 U 的值语义,并typedef 一个智能 Ptr

    template <class U>
    class CReferable: public virtual EReferable
    {
    private:
        U _u;
    public:
        friend class CReferable;
        CReferable(const CReferable& c) { _u = c._u; }
        CReferable(const U& u) { _u = u; }
        CReferable& operator=(const CReferable& c) { _u = c._u; return *this; }
        CReferable& operator=(const U& u) { _u = u; return *this; }
        operator U&() {return _u;}
        operator const U&() const {return _u;}

        typedef NTypes::Ptr<CReferable, XPtr> Ptr;
    };

测试单元

非常简单

using namespace GE_;

typedef NTypes::CReferable<int> CInt;
typedef NTypes::CReferable<double> CDouble;

int _tmain(int argc, _TCHAR* argv[])
{
    _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

    try
    {
        STRACE(trc,1,("Try Block\n"))
        CInt::Ptr pi;
        pi.New(5);

        int r = *pi;
        trc("r = %d\n", r);

        CDouble::Ptr pd = pi; //assign, but cannot convert int* to double*

        double d = *pd; //will throw

        trc("will never reach this message");
    }
    catch(NTypes::XException* pe)
    {
        STRACE(trc,1,("Catch Block\n"))
        pe;
    }

    trc("End Program\n");
    return 0;
}

//// 调试输出 ///

(   1).Begin test
(   1)..Try Block
(   1)..r = 5
(   0)...First reference for 002F0FF0
First-chance exception at 0x796be592 in CompVisit.exe: Microsoft C++ exception: 
    GE_::NTypes::Ptr<GE_::NTypes::CReferable<double>,
    GE_::NTypes::IReferable::XPtr>::exception @ 0x0012fbc0.
(   0)...Last release for 002F0FF0
(   1)..Catch Block
(   1).End Program
Il programma "[2100] CompVisit.exe: Nativo" è terminato con il codice 0 (0x0).

请注意,表达式 pd = pi 是合法的,因为 CInt::PtrCDouble::Ptr 依赖于 IReferable::XPtr,而 CIntCDouble 都是 EReferable

当所持有的值不是 CDouble 时,无法解引用 pd(例如使用 d = *pd):这会按设计抛出异常。

这显然是一个运行时类型转换错误,而不是模板中的静态转换。编译器无法检测到它,但我们可以支持多态,如本例所示

    CInt::Ptr pi, pj;
    CDouble::Ptr pd;
    pi.New(7);
    pd = pi; //legal: both convert to and from IReferable*

    pj = pd; //legal: both convert to and from IReferable*

    int r = *pj; //legal: pj holds a CInt

    pd.New(*pi); //legal: create a CDouble and initialized to a promoted integer

    double s = *pd; //legal: pd holds a CDouble

请注意,pjpd 接收了它从 pi 接收的内容,而 pi 创建了一个 CInt。使 pj 能够解引用的,是所持有的 IReferable 的性质。而不是传递值的来源。

Composite - Visitor

Composite 对象的行为可以用一个接口来描述:ITreeNode

    interface ITreeNode:
        public virtual IReferable
    {
        virtual ITreeNode* GetFirstChild()=0;
        virtual ITreeNode* GetLastChild()=0;
        virtual int GetChildrenCount()=0;
        virtual ITreeNode* GetParent()=0;
        virtual ITreeNode* GetPrev()=0;
        virtual ITreeNode* GetNext()=0;
        virtual bool SetParent(ITreeNode* pParent, 
                     ITreeNode* pPrevious=NULL)=0;
        virtual ~ITreeNode() {}

        typedef NTypes::Ptr<ITreeNode, IReferable::XPtr> Ptr;

        struct XShallowIterate
        {
            ...
        };

        typedef NTypes::Ptr<ITreeNode, XShallowIterate> ItShallow;

        struct XDeepIterate
        {
            ...
        };

        typedef NTypes::Ptr<ITreeNode, XDeepIterate> ItDeep;

    };

它定义了在父节点、子节点和兄弟节点之间导航以及设置这种结构所需的功能。

它还定义了自己的指针,用于浅层迭代(在兄弟节点之间迭代)和深层迭代(在子节点中迭代)的迭代器。

区别在于 `XShallowIterate` 和 `XDeepIterate` 中 `_inc` 和 `_dec` 函数的实现方式。

`_subscript` 函数也实现为重复迭代。

所有这些都提供了 `ItShallow` 和 `ItDeep` 迭代器,具有完美的 `++`、`--` 和 `[]` 运算符。

实现者的函数

虽然提供的函数从外部来看已经足够,但对于实现者来说却不够:如果我想实现 SetParent,我必须设置我对新父节点和兄弟节点的引用,但我还必须告诉父节点和兄弟节点设置它们对我的引用。

为此,有两种选择

  • 将此问题视为实现内部问题:这意味着实现者必须访问父节点和兄弟节点实现的相应数据结构。这可以通过 `class` 或 `struct` 中的受保护成员来实现,但这会使同一树中的所有对象必须使用或派生自相同的实现。
  • 将其视为实现外部问题,通过向接口添加更多函数。但这些函数不应被其他对象访问……除非是实现者。

第一个可能是“快速而粗糙的方法”。第二个是“正确的方法”。但有一个问题:这些函数应该如何声明?它们不能是 public,因为它们不应该被任何人调用。但它们必须被派生类的其他实例调用。甚至 protected 的方法也不好:它使得这些函数无法访问。

因此,我们需要一个代理对象(XProxy),由接口声明为 protected,但又是接口本身的 friend,它拥有公共函数:只有派生类才能获得这种代理,并且其函数可以调用接口的 protected 函数,这些函数将被派生实现覆盖。

请注意,拥有空接口定义抽象函数和操纵器,并带有可继承的部分实现(Exxx 类)的模型是多余的:它允许同一接口的不同实现;但如果您没有此类需求,则可以避免定义 Ixxx 接口,并直接从 Exxx 类开始部署。

这使您的灵活性降低了一个层次,但使您能够定义关于 Exxx*(而不是 Ixxx*)的函数,并且可以避免使用代理。

ITreeNode 实现:ETreeNode

由于 ITreeNode 需要 IReferable,因此 ETreeNode 可以继承…… EReferable,以便其实现可以支配 IReferable 虚基类。

    class ETreeNode: 
        public virtual ITreeNode,
        public virtual EReferable
    {
    public:
        virtual ITreeNode* GetFirstChild();
        virtual ITreeNode* GetLastChild();
        virtual ITreeNode* GetParent();
        virtual ITreeNode* GetPrev();
        virtual ITreeNode* GetNext();
        virtual bool SetParent(ITreeNode* pParent, 
                     ITreeNode* pPrevious=NULL);
    protected:
        virtual void SetFirstChild(ITreeNode* pN);
        virtual void SetLastChild(ITreeNode* pN);
        virtual void SetPrev(ITreeNode* pN);
        virtual void SetNext(ITreeNode* pN);
    protected:
        //hooks

        virtual bool OnSettingParent(ITreeNode* pParent, 
                     ITreeNode* pPrevious) {return true;}
        virtual void OnSettedParent() {}
    private:
        ITreeNode::Ptr _pChildFirst, _pChildLast, _pNext;
        ITreeNode *_pParent, *_pPrev; //backpointer don't refcount.

    protected:
        ETreeNode();
        virtual ~ETreeNode();
    };

注意 EReferable 设计与 ETreeNode 的区别

IReferable 具有所有私有函数,其私有 XPtr 为 friend,公共 Ptr 装饰 XPtr。这是因为我希望 IReferable::Ptr 是唯一可以操作 IReferable 函数的对象。

ITreeNode 具有一些公共函数,因为我希望这些函数可以从任何情况访问。但它也具有一些私有函数,以及一个受保护的代理作为 friend。这是为了让任何后代都能获得代理并通过它调用其函数。

SetParent 是唯一可以修改结构的函数。它通过两个钩子实现:OnSettingParentOnSettedParent

第一个返回一个 bool:如果返回 false,则拒绝设置父节点。这为实现对象之间某种“兼容性检查”提供了机会,这些对象被提名用于关联。

测试单元和示例

让我们想象一些形状,每个形状都有一个边界矩形。一个形状可以包含其他形状。然后我们可以有各种形状:特别是:矩形、圆形和组。

示例实现位于“shapes.h”。

每个形状都有自己的属性,并且默认情况下继承其所属组的属性。

    interface IShape:
        public virtual NTypes::ITreeNode
    {
        //define our own pointers and iterators

        typedef NTypes::Ptr<IShape, XPtr> Ptr;
        typedef NTypes::Ptr<IShape, XShallowIterate> ItShallow;
        typedef NTypes::Ptr<IShape, XDeepIterate> ItDeep;

        //defines our own public functions

        enum e_shapeattributes { attr_linecolor, 
             attr_fillcolor, attr_textcolor, attr__count };
        virtual NTypes::UINT GetAttribute(e_shapeattributes a) =0;
        virtual NTypes::UINT GetDeepAttribute(e_shapeattributes a) =0;
        virtual void SetAttribute(e_shapeattributes attr, NTypes::UINT value)=0;
        virtual void GetBound(SRect& r)=0;
        virtual void SetBound(const SRect& r)=0;
        virtual void Invalidate()=0;
    };

GetDeepAttribute 应该检索所需的属性,并且如果发现它等于 == -1,则从父组获取。

IShapeEShape 实现,它也继承 ETreeNode(用于实现 ITreeNode)。

EShape 派生出 EGraphicShape,它还继承 IDrawable 并重写 OnSettingParent 钩子,以阻止形状被设置为非 CGroup 对象的子对象。

最后,从 EGraphicShape 派生出 CCircleCRectangleCGroup。每个都有自己的 Draw 函数。

总而言之,这给出了此继承图

sample inheritance scheme

注意:我没有描绘 Ptr<...> 装饰器:任何类都可以拥有自己的装饰器,重新装饰 XPtrXIt……等等。

我还引入了 IDrawerCDrawer 作为什么都不做的可引用对象(只是为了有一个可以模拟 Windows 程序中 HDC 的东西)。

测试代码

我没有部署一个完整的 Windows 应用程序来绘制图形,因为本文的范围仅在于说明编码技术。但是一个控制台 main 可以是这样的

int _tmain(int argc, _TCHAR* argv[])
{
    _CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
    STRACE(trc, 1, ("Begin test\n"))

    //create a drawing

    NApp::CGroup::Ptr pDrawing;
    pDrawing.New();
    
    //add in it teo circles

    new NApp::CCircle(pDrawing, NApp::SRect(10,10,60,60));
    new NApp::CCircle(pDrawing, NApp::SRect(80,10,140,60), 0, 7);
    
    //create a subgroup

    NApp::CGroup* pSubDraw = new NApp::CGroup;
    pSubDraw->SetParent(pDrawing); //palce it into the drawing

    pSubDraw->SetAttribute(NApp::CGroup::attr_linecolor, 4);

    //add two rectangles in the subgroup

    new NApp::CRectangle(pSubDraw, NApp::SRect(20,70,130,120));
    new NApp::CRectangle(pSubDraw, NApp::SRect(20,125,130,150), 5);

    NApp::CDrawer::Ptr pDrw; pDrw.New();
    pDrawing->Draw(pDrw);

    std::cout << "\nPress any key ...\n";
    while(!getch());
    trc("End Program\n");
    return 0;
}

这会产生以下调试跟踪

(   1).Begin test
(   0)..First reference for 002F10E0
(   0)..First reference for 002F11A8
(   0)..First reference for 002F1278
(   0)..First reference for 002F29D0
(   0)..First reference for 002F2A98
(   0)..First reference for 002F2B68
(   0)..First reference for 002F2C38
(   2)..Drawing class GE_::NApp::CGroup at 3084512
(   2)...Drawing class GE_::NApp::CGroup at 3090896
(   2)....Drawing class GE_::NApp::CRectangle at 3091304
(   2)....Drawing class GE_::NApp::CRectangle at 3091096
(   2)...Drawing class GE_::NApp::CCircle at 3084920
(   2)...Drawing class GE_::NApp::CCircle at 3084712
(   1).End Program
(   0)..Last release for 002F2C38
(   0)...002F2C38 suicide
(   0)..Last release for 002F10E0
(   0)...002F10E0 suicide
(   0)....Last release for 002F29D0
(   0).....002F29D0 suicide
(   0)......Last release for 002F1278
(   0).......002F1278 suicide
(   0)........Last release for 002F11A8
(   0).........002F11A8 suicide
(   0)......Last release for 002F2B68
(   0).......002F2B68 suicide
(   0)........Last release for 002F2A98
(   0).........002F2A98 suicide

以及以下输出

Drawing group at 002F10E0 ...

Drawing group at 002F29D0 ...

Drawing rectangle at 002F2B68
        Line color = 5 Fill color = 2 Text color = 3
        placement: (20,125,130,150)

Drawing rectangle at 002F2A98
        Line color = 4 Fill color = 2 Text color = 3
        placement: (20,70,130,120)

end drawing group at 002F29D0 ...

Drawing circle at 002F1278
        Line color = 0 Fill color = 7
        placement: (80,10,140,60)

Drawing circle at 002F11A8
        Line color = 1 Fill color = 2
        placement: (10,10,60,60)

end drawing group at 002F10E0 ...


Press any key ...

注意:“绘图”是通过在 std::cout 上写入消息来实现的。

<H2>结论</H2>

代码被组织成一个 static 库和两个 using 它的项目。

该库名为 LTypes,项目名为 CompVisitReferableTest

当然,我指望这些项目有什么用。我希望这个说明可以作为多重虚继承和支配可以有效替代模板的示例,从而允许更“模块化”的代码,并且可以单独编译。

© . All rights reserved.