1

我有这个 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中,我重写了draw的代码,因为我没有继承它。在 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) {}
};

但是这里当然有问题。除非我从 Intf 派生它们,否则我无法从接口类访问 GetImpl()。如果我这样做,我需要让 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> {};
4

3 回答 3

4

我应该注意到,您的Impl课程只不过是在shared_ptr没有线程安全和所有这些演员奖励的情况下重新实现了 a 。

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实例),您还必须手动定义复制构造函数和赋值运算符。

您可能决定创建一个更好的类,该类shared_ptr将具有深拷贝语义(可以member_ptr在 cryptopp 中调用,或者只是Pimpl;))。虽然这引入了一个微妙的错误:虽然为复制构造函数和赋值运算符生成的代码可能被认为是正确的,但它们不是,因为您再次需要一个完整的类型(以及因此的定义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

您会看到:Circle 和 Rectangle 都不关心 Shape 是否使用 Pimpl,顾名思义,Pimpl 是一个实现细节,它是私有的,不与类的后代共享。

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

于 2009-10-28T16:34:52.207 回答
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;
};

另一种选择是创建一个更丰富的基接口类 - 它具有您需要的所有接口,然后为基类中的那些简单地定义一个默认的无操作实现,它返回 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 一样优雅)。

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

于 2009-10-28T17:41:14.430 回答
0

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

我认为您正在尝试将方形强制变成圆孔……它不太适合 C++。

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

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

注意在绝对最小值时,您仍然必须在 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 所需的虚拟成员函数。

如果实现更复杂,我可以手动将 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(); }
...
}

如果我需要 mix-in 来实际引用派生最多的类来与之交互,我可以向 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
};

最终,你的问题让我有些困惑。也许您可以创建一个示例来显示您的首选自然语法,它可以清楚地完成某些事情,因为我只是在您最初的帖子中没有看到这一点,而且我自己似乎无法从玩弄这些想法中找出它。

于 2009-10-29T19:41:49.037 回答