Добавление виртуальных функций без изменения исходных классов - PullRequest
6 голосов
/ 28 февраля 2010

Допустим, у нас уже есть иерархия классов, например,

class Shape { virtual void get_area() = 0; };
class Square : Shape { ... };
class Circle : Shape { ... };
etc.

Теперь давайте предположим, что я хочу (эффективно) добавить virtual draw() = 0 метод к Shape с соответствующими определениями в каждом подклассе. Однако , допустим, я хочу сделать это без изменения этих классов (так как они являются частью библиотеки, которую я не хочу изменять).

Как лучше всего это сделать?

Не важно, действительно ли я «добавляю» virtual метод или нет, я просто хочу, чтобы полиморфное поведение было задано массивом указателей.

Моей первой мыслью было бы сделать это:

class IDrawable { virtual void draw() = 0; };
class DrawableSquare : Square, IDrawable { void draw() { ... } };
class DrawableCircle : Circle, IDrawable { void draw() { ... } };

, а затем просто замените все создания Square с и Circle с на DrawableSquare с и DrawableCircle с, соответственно.

Это лучший способ сделать это, или есть что-то лучшее (желательно что-то, что оставляет создание Square s и Circle s без изменений).

Заранее спасибо.

Ответы [ 3 ]

5 голосов
/ 01 марта 2010

(я предлагаю решение дальше ... потерпите меня ...)

Одним из способов (почти) решения вашей проблемы является использование шаблона проектирования Visitor. Примерно так:

class DrawVisitor
{
public:
  void draw(const Shape &shape); // dispatches to correct private method
private:
  void visitSquare(const Square &square);
  void visitCircle(const Circle &circle);
};

Тогда вместо этого:

Shape &shape = getShape(); // returns some Shape subclass
shape.draw(); // virtual method

Вы бы сделали:

DrawVisitor dv;
Shape &shape = getShape();
dv.draw(shape);

Обычно в шаблоне Visitor вы реализуете метод draw следующим образом:

DrawVisitor::draw(const Shape &shape)
{
  shape.accept(*this);
}

Но это работает только в том случае, если иерархия Shape была разработана для посещения: каждый подкласс реализует виртуальный метод accept, вызывая соответствующий метод visitXxxx в Visitor. Скорее всего, он не был предназначен для этого.

Не имея возможности изменить иерархию классов для добавления виртуального метода accept к Shape (и всем подклассам), вам необходим какой-то другой способ отправки в правильный метод draw. Один наивный подход заключается в следующем:

DrawVisitor::draw(const Shape &shape)
{
  if (const Square *pSquare = dynamic_cast<const Square *>(&shape))
  {
    visitSquare(*pSquare);
  }
  else if (const Circle *pCircle = dynamic_cast<const Circle *>(&shape))
  {
    visitCircle(*pCircle);
  }
  // etc.
}

Это сработает, но при использовании dynamic_cast наблюдается снижение производительности. Если вы можете позволить себе этот удар, это простой подход, который легко понять, отладить, поддерживать и т. Д.

Предположим, что было перечисление всех типов фигур:

enum ShapeId { SQUARE, CIRCLE, ... };

и был виртуальный метод ShapeId Shape::getId() const = 0;, который каждый подкласс переопределил бы, чтобы вернуть ShapeId. Тогда вы можете выполнить отправку, используя массивный оператор switch вместо if-elsif-elsif, равного dynamic_cast s. Или, возможно, вместо switch используйте хеш-таблицу. В лучшем случае нужно поместить эту функцию сопоставления в одном месте, чтобы вы могли определить несколько посетителей, не повторяя каждый раз логику сопоставления.

Так что у вас, вероятно, нет и метода getid(). Очень плохо. Как еще можно получить идентификатор, уникальный для каждого типа объекта? RTTI. Это не обязательно элегантно или надежно, но вы можете создать хеш-таблицу из type_info указателей. Вы можете создать эту хеш-таблицу в некотором коде инициализации или построить его динамически (или оба).

DrawVisitor::init() // static method or ctor
{
  typeMap_[&typeid(Square)] = &visitSquare;
  typeMap_[&typeid(Circle)] = &visitCircle;
  // etc.
}

DrawVisitor::draw(const Shape &shape)
{
  type_info *ti = typeid(shape);
  typedef void (DrawVisitor::*VisitFun)(const Shape &shape);
  VisitFun visit = 0; // or default draw method?
  TypeMap::iterator iter = typeMap_.find(ti);
  if (iter != typeMap_.end())
  {
    visit = iter->second;
  }
  else if (const Square *pSquare = dynamic_cast<const Square *>(&shape))
  {
    visit = typeMap_[ti] = &visitSquare;
  }
  else if (const Circle *pCircle = dynamic_cast<const Circle *>(&shape))
  {
    visit = typeMap_[ti] = &visitCircle;
  }
  // etc.

  if (visit)
  {
    // will have to do static_cast<> inside the function
    ((*this).*(visit))(shape);
  }
}

Там могут быть некоторые ошибки / синтаксические ошибки, я не пробовал компилировать этот пример. Я делал что-то подобное раньше - техника работает. Я не уверен, что у вас могут возникнуть проблемы с общими библиотеками.

И последнее, что я добавлю: независимо от того, как вы решите отправлять сообщения, возможно, имеет смысл создать базовый класс для посетителей:

class ShapeVisitor
{
public:
  void visit(const Shape &shape); // not virtual
private:
  virtual void visitSquare(const Square &square) = 0;
  virtual void visitCircle(const Circle &circle) = 0;
};
3 голосов
/ 28 февраля 2010

То, что вы описываете, похоже на шаблон декоратора . Что очень удобно для изменения поведения существующих классов во время выполнения.

Но я не очень понимаю, как реализовать ваш практический пример, если фигуры не могут быть нарисованы, то и во время выполнения изменить способ рисования невозможно ...

Но я полагаю, это просто очень упрощенный пример для работы со стеком? Если доступны все основные строительные блоки для требуемой функциональности, то реализация точного поведения во время выполнения с таким шаблоном, безусловно, является достойным вариантом.

0 голосов
/ 01 марта 2010

Одним из решений «вне стены», которое вы можете рассмотреть, в зависимости от обстоятельств, является использование шаблонов, чтобы дать вам полиморфное поведение времени компиляции. Прежде чем что-то сказать, я знаю , что это не даст вам традиционного полиморфизма во время выполнения, поэтому он может быть бесполезен, но в зависимости от ограничений среды, в которой вы работаете, он может оказаться полезным: 1003 *

#include <iostream>

using namespace std;

// This bit's a bit like your library.
struct Square{};
struct Circle{};
struct AShape{};

// and this is your extra stuff.
template < class T >
class Drawable { public: void draw() const { cout << "General Shape" << endl; } };

template <> void Drawable< Square >::draw() const { cout << "Square!" << endl; };
template <> void Drawable< Circle >::draw() const { cout << "Circle!" << endl; };

template < class T >
void drawIt( const T& obj )
{
  obj.draw();
}

int main( int argc, char* argv[] )
{
  Drawable<Square> a;
  Drawable<Circle> b;
  Drawable<AShape> c;

  a.draw(); // prints "Square!"
  b.draw(); // prints "Circle!"
  c.draw(); // prints "General Shape" as there's no specific specialisation for an Drawable< AShape >

  drawIt(a); // prints "Square!"
  drawIt(b); // prints "Circle!"
  drawIt(c); // prints "General Shape" as there's no specific specialisation for an Drawable< AShape >
}

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...