Проблема PIMPL: как создать несколько интерфейсов для дублирования кода без - PullRequest
1 голос
/ 28 октября 2009

У меня есть такой дизайн 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) {}
};

Но, конечно, здесь есть проблема. Я не могу получить доступ к 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 ]

4 голосов
/ 28 октября 2009

Я должен отметить, что ваш Impl класс - не что иное, как переопределение shared_ptr без безопасности потока и всех этих бонусов приведения.

Pimpl - не что иное, как техника, позволяющая избежать ненужных зависимостей времени компиляции.

Вам не нужно знать, как реализован класс для его наследования. Это победило бы цель инкапсуляции (хотя ваш компилятор делает ...).

Итак ... я думаю, что вы не пытаетесь использовать Pimpl здесь. Я бы скорее подумал, что это своего рода шаблоны Proxy, так как, по-видимому:

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

В классической реализации нет большой разницы, как вы можете видеть, только в том, что вместо группы данных есть указатель.

BEWARE

Если вы отправите объявление DerivedImpl (которое является целью Pimpl), то деструктор, автоматически сгенерированный компилятором, будет ... неправильным.

Проблема в том, что для генерации кода для деструктора компилятору необходимо определить DerivedImpl (т. Е. Полный тип), чтобы знать, как его уничтожить, поскольку вызов delete скрыт в недра shared_ptr. Однако он может генерировать предупреждение только во время компиляции (но у вас будет утечка памяти).

Более того, если вам нужна более полная копия (а не мелкая копия, состоящая из копии и оригинала, указывающих на один и тот же экземпляр DerivedImpl), вам также придется определить конструктор копирования вручную И оператор присваивания.

Вы можете решить создать лучший класс, чем shared_ptr, который будет иметь семантику глубокой копии (которую можно назвать member_ptr как в cryptopp или просто Pimpl;)). Это создает небольшую ошибку: хотя код, сгенерированный для конструктора копирования и оператора присваивания, можно считать корректным, это не так, поскольку вам снова нужен полный тип (и, следовательно, определение DerivedImpl), поэтому вам придется написать их вручную.

Это больно ... и мне жаль.

РЕДАКТИРОВАТЬ: Давайте поговорим о Shape.

// 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, каждый со своим собственным «классом реализации» (который, кстати, может быть не более чем простой структурой без метода).

0 голосов
/ 29 октября 2009

Я думаю, вы были правы в том, что изначально я не поняла ваш вопрос.

Я думаю, что вы пытаетесь втиснуть квадратную форму в круглое отверстие ... оно не совсем подходит для C ++.

Вы можете принудить, чтобы ваш контейнер содержал указатели на объекты данного базового макета, а затем разрешить фактическое указание объектов произвольной композиции оттуда, предполагая, что вы, как программист, только фактически размещаете объекты, которые на самом деле имеют идентичные макеты памяти (данные-члены - для класса нет такого понятия, как макет-функция-член, если у него нет виртуальных объектов, которых вы хотите избежать).

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

ПРИМЕЧАНИЕ: при абсолютном МИНИМАЛЕ у вас все еще должен быть виртуальный деструктор, определенный в IShape, иначе удаление объекта будет с треском провалено

И у вас могут быть классы, которые все получают указатель на общее ядро ​​реализации, так что все композиции можно инициализировать с помощью элемента, который они совместно используют (или это можно сделать статически в виде шаблона с помощью указателя - общих данных).

Но дело в том, что, если я пытаюсь создать пример, я теряю равновесие в секунду, когда пытаюсь рассмотреть: какие данные используются всеми формами? Я полагаю, у вас может быть вектор точек, который может быть таким же большим или маленьким, как требуется для любой фигуры. Но даже в этом случае Draw () является по-настоящему полиморфным, это не реализация, которая может совместно использоваться несколькими типами - она ​​должна быть настроена для различных классификаций фигур. то есть круг и многоугольник не могут совместно использовать один и тот же Draw (). А без vtable (или какой-либо другой конструкции указателя динамической функции) вы не можете отличить функцию, вызываемую от какой-либо общей реализации или клиента.

Ваш первый набор кода полон запутанных конструкций. Может быть, вы можете добавить новый, упрощенный пример, который PURELY показывает - более реалистично - что вы пытаетесь сделать (и игнорируете тот факт, что в 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.

Я могу, если реализации были более сложными, вручную связать виртуальную таблицу с унаследованным разработчиком, если я захочу:

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 не мог сам предоставить?

Какие общие элементы мог бы иметь любой составной класс, к которому различные миксины хотели бы иметь доступ, который они должны были извлечь из себя? Просто пусть 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
};

В конечном итоге ваш вопрос по-прежнему несколько смущает меня. Возможно, вы могли бы создать этот пример, который демонстрирует ваш предпочтительный естественный синтаксис, который что-то четко выполняет, поскольку я просто не вижу этого в вашем первоначальном посте, и я не могу вычеркнуть его из игры с этими идеями. 1042 *

0 голосов
/ 28 октября 2009

Я видел множество решений этой основной головоломки: полиморфизм + вариация в интерфейсах.

Один из основных подходов состоит в том, чтобы предоставить способ запроса расширенных интерфейсов - так что у вас есть что-то вроде программирования COM под Windows:

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, из того, что я видел).

Во всяком случае, ... удачи:)

...