Добавление виртуальных функций, не изменяя исходные классы

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

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

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

Каков был бы лучший способ пойти об этом?

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

Моя первая мысль состояла бы в том, чтобы сделать это:

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

и затем просто замените все создания Squares и Circles с DrawableSquares и DrawableCircles, соответственно.

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

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

6
задан Peter Alexander 28 February 2010 в 20:28
поделиться

3 ответа

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

Один из способов (почти) решить вашу проблему - использовать шаблон проектирования 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);

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

Так что, вероятно, у вас тоже нет метода 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;
};
5
ответ дан 16 December 2019 в 21:38
поделиться

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

#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 () . Обратите внимание на раздувание кода здесь, поскольку компилятор создает отдельный метод для каждого переданного типа.

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

0
ответ дан 16 December 2019 в 21:38
поделиться

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

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

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

3
ответ дан 16 December 2019 в 21:38
поделиться
Другие вопросы по тегам:

Похожие вопросы: