PIMPL问题:如何将多个接口与impl w / o代码重复

时间:2009-10-28 16:11:15

标签: c++ interface pimpl-idiom

我有这个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);
};

这是问题所在。类Polygon有两种可能的声明:

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);
    }
}

在Polygon1中,我已经重写了绘制代码,因为我还没有继承它。在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) {}
};

但当然这里有一个问题。我无法从Interface类访问GetImpl(),除非我从Intf派生它们。如果我这样做,我需要在出现的任何地方使Intf虚拟化。

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

class Polygon : public virtual Intf<PolygonImpl>, public PolygonInterface { ... }

或者我可以存储TImpl *&amp;在每个接口中,并使用对基础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 个答案:

答案 0 :(得分:4)

我应该注意,你的Impl课程只不过是shared_ptr的重新实现而没有线程安全和所有那些演员奖金。

Pimpl只是一种技术,可以避免不必要的编译时依赖。

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

所以...我认为你不是想在这里使用Pimpl。我宁愿认为这是一种代理模式,因为显然:

Polygon1 numberOne;
Polygon2 numberTwo = numberOne;

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

如果您想隐藏实施细节

使用Pimpl,但是真实的,它意味着在复制构造和赋值期间进行深度复制而不是仅仅传递指针(无论是否重新计数,尽管当然重新计算是优选的:) )。

如果您需要代理类

只需使用普通shared_ptr

继承

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

如果你想添加一些新的私人成员(通常情况下),那么:

struct DerivedImpl;

class Derived: public Base // Base implemented with a Pimpl
{
public:

private:
  std::shared_ptr<DerivedImpl> _data;
};

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

<强>提防

如果你转发声明DerivedImpl(这是Pimpl的目标),那么编译器自动生成的析构函数是......错误的。

问题是,为了生成析构函数的代码,编译器需要DerivedImpl的定义(即:一个完整​​的类型)才能知道如何销毁它,因为对delete的调用是隐藏在shared_ptr的内容中。但是它可能只在编译时生成警告(但是你会有内存泄漏)。

此外,如果你想要一个深入的副本(而不是一个浅的副本,它包含在副本中,而原始副本都指向同一个DerivedImpl实例),你还需要手动定义副本-constructor和赋值运算符。

您可能决定创建一个更好的类shared_ptr,它将具有深层复制语义(可以像cryptopp一样被称为member_ptr,或只是Pimpl;))。这引入了一个微妙的错误:虽然为复制构造函数和assignement运算符生成的代码可以被认为是正确的,但它们不是,因为再一次你需要一个完整的类型(因此DerivedImpl的定义),所以你必须手动编写它们。

这很痛苦......我很抱歉。

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

// 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,Circle和Rectangle都不关心,顾名思义,Pimpl是一个实现细节,私有的,不与类的后代共享。

正如我解释的那样,Circle和Rectangle也都使用Pimpl,每个都有自己的'实现类'(顺便说一句,它只不过是一个没有方法的简单结构)。

答案 1 :(得分:0)

我已经看到了很多解决这个基本难题的方法:多态性+接口变异。

一种基本方法是提供一种查询扩展接口的方法 - 所以你在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;
};

另一种选择是创建一个更丰富的基本接口类 - 它具有您需要的所有接口,然后简单地为基类中的那些定义默认的no-op实现,返回false或throws来指示所讨论的子类不支持它(否则子类将为该成员函数提供功能实现)。

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具有能够向元类型系统询问给定实例是否支持特定方法的奇妙属性 - 并且它支持具有可以在给定实例被告知执行操作的任何时候执行的类处理程序不支持 - 以及那些操作,所以你可以将它作为代理转发,或者你可以抛出一个不同的错误,或者只是静静地忽略该操作作​​为无操作。)

Objective-C支持所有与Smalltalk相同的模式!通过在运行时访问类型系统,可以实现非常非常酷的事情。我认为.NET可以在这些方面提取一些疯狂的东西(尽管我怀疑它几乎和Smalltalk或Objective-C一样优雅,从我所见过的。)

无论如何,......祝你好运:)

答案 2 :(得分:0)

我认为你是对的,因为我最初并不理解你的问题。

我认为你试图将方形形状强制成圆孔......它不太适合C ++。

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

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

注意,在绝对MINIMUM中,你必须仍然在IShape中定义一个虚拟析构函数,否则对象删除将失败惨不忍睹

并且您可以拥有所有类都指向通用实现核心的类,以便可以使用它们共享的元素初始化所有组合(或者可以通过指针静态地将其作为模板 - 共享数据)。

但问题是,如果我试图创建一个例子,我会试着考虑的第二个问题:所有形状共享的数据是什么?我想你可以有一个点矢量,然后可以像所需的任何形状一样大或小。但即便如此,Draw()确实是多态的,它不是可以由多种类型共享的实现 - 它必须针对各种形状分类进行自定义。即圆和多边形不可能共享相同的Draw()。如果没有vtable(或其他一些动态函数指针构造),则无法改变从某些常见实现或客户端调用的函数。

你的第一组代码充满了令人困惑的结构。也许你可以添加一个新的,简化的例子,以更加真实的方式 - 以更现实的方式 - 你想要做的事情(而忽略了C ++没有你想要的机制这一事实 - 只是展示你的机制应该是什么样子)。

在我看来,我只是没有得到实际的实际应用,除非你要做类似以下的事情:

获取一个COM类,它继承自另外两个COM接口:

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

现在我有一个钻石继承模式:IShellBrowser最终从IUnknown继承,ICommDlgBrowser也是如此。但是编写我自己的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填充了所需的虚拟成员函数。 / p>

如果实现更复杂,我可以手动将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添加一个模板参数,让它可以访问自己。

但是IUnknownMixin本身不能提供哪些共同元素可以让我的班级拥有或受益呢?

任何复合类都有哪些常见元素可以访问各种mixin,他们需要从这些元素派生出来?只需让mixins获取一个类型参数并访问它。如果它的实例数据是派生最多的,那么你有类似的东西:

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
};

最终你的问题对我来说仍然有点混乱。也许你可以创建那个显示你首选的自然语法的例子,它可以清楚地完成某些事情,因为我在你的帖子中没有看到这一点,而且我自己似乎没有从玩弄这些想法中解脱出来。 / p>