代码之家  ›  专栏  ›  技术社区  ›  jmucchiello

PIMPL问题:如何拥有多个与IMPL的接口,而不需要代码复制

  •  1
  • jmucchiello  · 技术社区  · 15 年前

    我有一个PIMPL设计,其中实现类是多态的,但是接口应该只包含一个指针,这使得它们的多态性有点破坏了设计的目的。

    所以我创建了impl和intf基类来提供引用计数。然后用户可以创建他们的实现。一个例子:

    class Impl {
        mutable int _ref;
    public:
        Impl() : _ref(0) {}
        virtual ~Impl() {}
    
        int addRef() const { return ++_ref; }
        int decRef() const { return --_ref; }
    };
    
    template <typename TImpl>
    class Intf {
        TImpl* impl;
    public:
        Intf(TImpl* t = 0) : impl(0) {}
        Intf(const Intf& other) : impl(other.impl) { if (impl) impl->addRef(); }
        Intf& operator=(const Intf& other) {
             if (other.impl) other.impl->addRef();
             if (impl && impl->decRef() <= 0) delete impl;
             impl = other.impl;
        }
        ~Intf() { if (impl && impl->decRef() <= 0) delete impl; }
    protected:
        TImpl* GetImpl() const { return impl; }
        void SetImpl(... //etc
    };
    
    class ShapeImpl : public Impl {
    public:
        virtual void draw() = 0;
    };
    
    class Shape : public Intf<ShapeImpl> {
    public:
        Shape(ShapeImpl* i) : Intf<ShapeImpl>(i) {}
    
        void draw() {
             ShapeImpl* i = GetImpl();
             if (i) i->draw();
        }
    };
    
    class TriangleImpl : public ShapeImpl {
    public:
        void draw();
    };
    
    class PolygonImpl : public ShapeImpl {
    public:
        void draw();
        void addSegment(Point a, Point b);
    };
    

    这就是问题所在。类多边形有两种可能的声明:

    class Polygon1 : public Intf<PolygonImpl> {
    public:
        void draw() {
             PolygonImpl* i = GetImpl();
             if (i) i->draw();
        }
        void addSegment(Point a, Point b) {
            PolygonImpl* i = GetImpl();
            if (i) i->addSegment(a,b);
        }
    };
    
    class Polygon2 : public Shape {
        void addSegment(Point a, Point b) {
            ShapeImpl* i = GetImpl();
            if (i) dynamic_cast<Polygon*>(i)->addSegment(a,b);
        }
    }
    

    在多边形1中,我重写了用于绘制的代码,因为我没有继承它。在polygon2中,我需要丑陋的动态强制转换,因为getImpl()不知道polygonImpl。我想做的是这样的事情:

    template <typename TImpl>
    struct Shape_Interface {
        void draw() {
            TImpl* i = GetImpl();
            if (i) i->draw();
        }
    };
    
    template <typename TImpl>
    struct Polygon_Interface : public Shape_Interface<Timpl> {
        void addSegment(Point a, Point b) { ... }
    };
    
    class Shape : public TIntf<ShapeImpl>, public Shape_Interface<ShapeImpl> {...};
    
    class Polygon : public TIntf<PolygonImpl>, public Polygon_Interface<PolygonImpl> {
    public:
        Polygon(PolygonImpl* i) : TIntf<PolygonImpl>(i) {}
    };
    

    但这里当然有问题。我不能从接口类访问getimpl(),除非我从intf派生它们。如果我这样做的话,我需要使intf在它出现的任何地方都是虚拟的。

    template <typename TImpl>
    class PolygonInterface : public virtual Intf<TImpl> { ... };
    
    class Polygon : public virtual Intf<PolygonImpl>, public PolygonInterface { ... }
    

    或者,我可以在每个接口中存储一个timpl*&并使用对基intf::impl的引用来构造它们。但这仅仅意味着对于包含的每个接口,我都有一个指向自己的指针。

    template <typename TImpl>
    class PolygonInterface {
        TImpl*& impl;
    public:
        PolygonInterface(TImpl*& i) : impl(i) {}
    ...};
    

    这两个解决方案都会使intf类膨胀,增加额外的引用,并且基本上不会比直接多态性带来任何好处。

    所以,问题是,除了到处复制代码(包括维护问题),还有第三种方法可以解决这个问题吗?

    完全应该,但不起作用:我希望有一些基类联合覆盖类布局,对于多态类,要求它们具有完全相同的vtable布局。然后,intf和shapeinterface都将声明一个T*元素,并以相同的方式访问它:

    class Shape : public union Intf<ShapeImpl>, public union ShapeInterface<ShapeImpl> {};
    
    3 回复  |  直到 15 年前
        1
  •  0
  •   Mordachai    15 年前

    我认为你是对的,因为我一开始不理解你的问题。

    我想你是想把一个方形的东西压成一个圆孔…它不太适合C++。

    可以 强制容器保存指向给定基本布局对象的指针,然后允许从那里实际指向任意组合的对象,假设您作为程序员实际上只放置具有相同内存布局的对象(成员数据-没有类的成员函数布局,除非它有虚拟的,你希望避免它)。

    std::vector< boost::shared_ptr<IShape> > shapes;  
    

    注意:在绝对最小的情况下,您必须在ishape中定义一个虚拟析构函数,否则对象删除将失败。

    你可以有一个类,所有这些类都有一个指向公共实现核心的指针,这样所有的组合都可以用它们共享的元素进行初始化(或者可以通过指针静态地作为一个模板来完成——共享数据)。

    但问题是,如果我试图创建一个示例,我会在考虑到:所有形状共享的数据是什么?我想你可以有一个点的向量,它可以是任何形状所需要的大小。但即使如此,draw()确实是多态的,它不是一个可能由多个类型共享的实现——它必须为各种形状分类定制。也就是说,一个圆和一个多边形不可能共享同一个draw()。如果没有vtable(或其他动态函数指针构造),就不能改变从一些公共实现或客户机调用的函数。

    您的第一组代码充满了混乱的结构。也许你可以添加一个新的,简单的例子,纯粹以一种更现实的方式显示——你想做的事情(忽略C++没有你想要的机制——只演示你的机械师应该是什么样子)。

    在我看来,我只是没有得到实际的应用程序,除非你正准备做如下的事情:

    采用从其他两个COM接口继承的COM类:

    class MyShellBrowserDialog : public IShellBrowser, public ICommDlgBrowser
    {
      ...
    };
    

    现在我有了一个菱形继承模式:ishellbrowser和icommdlgbrowser最终从iunknown继承。但是,编写自己的iunknown:addref和iunknown::release实现似乎非常愚蠢,这是一种高度标准的实现,因为没有办法让编译器让另一个继承的类为ishellbrowser和/或icommdlgbrowser提供缺少的虚拟函数。

    也就是说,我最终不得不:

    class MyShellBrowserDialog : public IShellBrowser, public ICommDlgBrowser
    {
    public:
     virtual ULONG STDMETHODCALLTYPE AddRef(void) { return ++m_refcount; }
     virtual ULONG STDMETHODCALLTYPE Release(void) { return --m_refcount; }
    ...
    }
    

    因为我不知道从其他地方“继承”或“注入”那些函数实现到myshellbrowserdialog中。 它实际上填充了所需的虚拟成员函数 对于IShellBrowser或ICommDlgBrowser。

    如果实现更复杂,我可以手动将vtable链接到继承的实现者,如果我愿意:

    class IUnknownMixin
    {
     ULONG m_refcount;
    protected:
     IUnknonwMixin() : m_refcount(0) {}
    
     ULONG AddRef(void) { return ++m_refcount; } // NOTE: not virutal
     ULONG Release(void) { return --m_refcount; } // NOTE: not virutal
    };
    
    class MyShellBrowserDialog : public IShellBrowser, public ICommDlgBrowser, private IUnknownMixin
    {
    public:
     virtual ULONG STDMETHODCALLTYPE AddRef(void) { return IUnknownMixin::AddRef(); }
     virtual ULONG STDMETHODCALLTYPE Release(void) { return IUnknownMixin::Release(); }
    ...
    }
    

    如果我需要混入来实际引用最派生的类来与它交互,我可以向iunknownmixin添加一个模板参数,以使它能够访问我自己。

    但是,我的班级能有什么共同的元素,或者说我的学生能从这些元素中获益,而这些元素本身并不能提供?

    任何复合类都可以拥有哪些不同的混音器想要访问的公共元素,这些元素是它们从自身派生出来的?让mixin接受一个类型参数并访问它。如果它的实例数据是最派生的,那么您有如下内容:

    template <class T>
    class IUnknownMixin
    {
     T & const m_outter;
    protected:
     IUnknonwMixin(T & outter) : m_outter(outter) {}
     // note: T must have a member m_refcount
    
     ULONG AddRef(void) { return ++m_outter.m_refcount; } // NOTE: not virtual
     ULONG Release(void) { return --m_outter.m_refcount; } // NOTE: not virtual
    };
    

    最终你的问题对我来说还是有些困惑。也许你可以创建一个例子来展示你喜欢的自然语法,它能清楚地完成一些事情,因为我在你最初的文章中没有看到这个,而且我自己似乎也无法从玩弄这些想法中找到它。

        2
  •  4
  •   Matthieu M.    15 年前

    我应该注意到 Impl 类只不过是对 shared_ptr 没有线程安全性和所有这些演员奖金。

    PIMPL只是一种避免不必要的编译时依赖性的技术。

    您不需要实际知道如何实现类来继承它。它会破坏封装的目的(尽管编译器会…)。

    所以…我想你不是想用皮条客。我宁愿认为这是一种代理模式,因为显然:

    Polygon1 numberOne;
    Polygon2 numberTwo = numberOne;
    
    numberTwo.changeData(); // affects data from numberOne too
                            // since they point to the same pointer!!
    

    如果要隐藏实现详细信息

    使用 Pimpl 但实际上,这意味着在复制构造和分配过程中进行深度复制,而不是仅仅传递指针(无论引用是否计数,尽管引用计数当然更好:)。

    如果需要代理类

    用素的就行了 SelddPPTR .

    为了继承

    当您从类继承时,它的私有成员是如何实现的并不重要。所以就从中继承吧。

    如果要添加一些新的私人成员(通常情况下),则:

    struct DerivedImpl;
    
    class Derived: public Base // Base implemented with a Pimpl
    {
    public:
    
    private:
      std::shared_ptr<DerivedImpl> _data;
    };
    

    正如您所看到的,经典实现没有太大的区别,只是有一个指针代替了一堆数据。

    当心

    如果你转寄申报单 DerivedImpl (这是PIMPL的目标),那么编译器自动生成的析构函数是……错了。

    问题是为了生成析构函数的代码,编译器需要定义 导出二聚体 (即:一个完整的类型)为了知道如何销毁它,因为删除调用隐藏在共享资源的内部。但是,它可能只在编译时生成一个警告(但您将出现内存泄漏)。

    此外,如果您想要一份深入的副本(而不是一份浅薄的副本,它包含在副本和原始副本中,两者都指向同一个副本) 导出二聚体 实例),您还必须手动定义复制构造函数和赋值运算符。

    你可以决定创建一个更好的类 SelddPPTR 它将具有深度复制语义(可以称为 member_ptr 就像在Cryptopp,或者只是 平普尔 ;)这引入了一个微妙的错误:虽然为复制构造函数和赋值运算符生成的代码可以被认为是正确的,但它们不是,因为您再次需要一个完整的类型(因此 派生模板 ,因此您必须手动编写它们。

    这很痛苦…我为你感到抱歉。

    编辑:让我们进行形状讨论。

    // Shape.h
    namespace detail { class ShapeImpl; }
    
    class Shape
    {
    public:
      virtual void draw(Board& ioBoard) const = 0;
    private:
      detail::ShapeImpl* m_impl;
    }; // class Shape
    
    
    // Rectangle.h
    namespace detail { class RectangleImpl; }
    
    class Rectangle: public Shape
    {
    public:
      virtual void draw(Board& ioBoard) const;
    
      size_t getWidth() const;
      size_t getHeight() const;
    private:
      detail::RectangleImpl* m_impl;
    }; // class Rectangle
    
    
    // Circle.h
    namespace detail { class CircleImpl; }
    
    class Circle: public Shape
    {
    public:
      virtual void draw(Board& ioBoard) const;
    
      size_t getDiameter() const;
    private:
      detail::CircleImpl* m_impl;
    }; // class Circle
    

    您可以看到:圆形和矩形都不关心shape是否使用pimpl,正如其名称所暗示的那样,pimpl是一个实现细节,它是一个私有的东西,不能与类的后代共享。

    正如我所解释的,圆和矩形也都使用pimpl,每个都有自己的“实现类”(顺便说一下,它只能是一个没有方法的简单结构)。

        3
  •  0
  •   Mordachai    15 年前

    我已经看到许多解决这个基本难题的方法:多态性+界面的变化。

    一种基本的方法是提供一种查询扩展接口的方法,因此您可以在Windows下使用COM编程的方法:

    const unsigned IType_IShape = 1;
    class IShape
    {
    public:
        virtual ~IShape() {} // ensure all subclasses are destroyed polymorphically!
    
        virtual bool isa(unsigned type) const { return type == IType_IShape; }
    
        virtual void Draw() = 0;
        virtual void Erase() = 0;
        virtual void GetBounds(std::pair<Point> & bounds) const = 0;
    };
    
    
    const unsigned IType_ISegmentedShape = 2;
    class ISegmentedShape : public IShape
    {
    public:
        virtual bool isa(unsigned type) const { return type == IType_ISegmentedShape || IShape::isa(type); }
    
        virtual void AddSegment(const Point & a, const Point & b) = 0;
        virtual unsigned GetSegmentCount() const = 0;
    };
    
    class Line : public IShape
    {
    public:
        Line(std::pair<Point> extent) : extent(extent) { }
    
        virtual void Draw();
        virtual void Erase();
        virtual void GetBounds(std::pair<Point> & bounds);
    
    private:
        std::pair<Point> extent;
    };
    
    
    class Polygon : public ISegmentedShape
    {
    public:
        virtual void Draw();
        virtual void Erase();
        virtual void GetBounds(std::pair<Point> & bounds);
        virtual void AddSegment(const Point & a, const Point & b);
        virtual unsigned GetSegmentCount() const { return vertices.size(); }
    
    private:
        std::vector<Point> vertices;
    };
    

    另一种选择是创建一个更丰富的基本接口类——它拥有您所需要的所有接口,然后简单地为基类中的那些接口定义一个默认的、没有op实现,它返回false或throw来表示所讨论的子类不支持它(否则子类将提供一个函数implem此成员函数的纠缠)。

    class Shape
    {
    public:
    
        struct Unsupported
        {
            Unsupported(const std::string & operation) : bad_op(operation) {}
    
            const std::string & AsString() const { return bad_op; }
    
            std::string bad_op;
        };
    
    
        virtual ~Shape() {} // ensure all subclasses are destroyed polymorphically!
    
        virtual void Draw() = 0;
        virtual void Erase() = 0;
        virtual void GetBounds(std::pair<Point> & bounds) const = 0;
        virtual void AddSegment(const Point & a, const Point & b) { throw Unsupported("AddSegment"); }
        virtual unsigned GetSegmentCount() const { throw Unsupported("GetSegmentCount"); }
    };
    

    我希望这能帮助你看到一些可能性。

    smalltalk有一个极好的特性,即能够询问元类型系统给定的实例是否支持特定的方法,并且它支持类处理程序,该类处理程序可以在任何时候要求给定的实例执行它不支持的操作时执行,以及该操作是什么,因此您可以将其作为代理转发,或者您可以抛出一个不同的错误,或者只是悄悄地忽略该操作作为一个no-op)。

    Objective-C支持与Smalltalk相同的所有模式!非常非常酷的事情可以通过在运行时访问类型系统来完成。我假设.NET可以沿着这些线拉一些疯狂的酷东西(尽管从我所看到的来看,它几乎和Smalltalk或Objective-C一样优雅)。

    无论如何…祝你好运:

    推荐文章