Подтвердить что ты не робот

Шаблон, чтобы избежать dynamic_cast

У меня есть класс:

class A 
{
public:
  virtual void func() {...}
  virtual void func2() {...}
};

И некоторые производные классы из этого, скажем, B, C, D... В 95% случаев я хочу пройти через все объекты и вызвать func или func2(), поэтому поэтому я их в вектор, например:

std::vector<std::shared_ptr<A> > myVec;
...
for (auto it = myVec.begin(); it != myVec.end(); ++it)
  (*it).func();

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

  • Используйте dynamic_cast для анализа подкласса. Не очень хорошо, слишком медленно, поскольку я делаю звонки очень часто и на ограниченном оборудовании.
  • Используйте флаг в каждом подклассе, например enum {IS_SUBCLASS_B, IS_SUBCLASS_C}. Нехорошо, так как он не чувствует OO.
  • Также поместите классы в другие векторы, каждый из которых выполняет свою конкретную задачу. Это тоже не очень хорошо, но, возможно, я здесь неправ. Как:

    std::vector<std::shared_ptr<B> > vecForDoingSpecificOperation;
    std::vector<std::shared_ptr<C> > vecForDoingAnotherSpecificOperation;
    

Итак, может кто-нибудь предложить стиль/шаблон, который достигает того, что я хочу?

4b9b3361

Ответ 1

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

Там, где один лучше, и случаи, когда другой. Обычно эмпирическое правило выглядит следующим образом:

  • Если у вас есть довольно фиксированный набор операций, но сохраняйте добавление типов, используйте виртуальные функции.
    Операции трудно добавить в/удалить из большой иерархии наследования, но новые типы легко добавить, просто заставив их переопределить соответствующие виртуальные функции.

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

(Если оба изменения, вы обречены в любом случае.)

Ответ 2

Согласно вашим комментариям, то, что вы наткнулись, известно (сомнительно) как "Проблема выражения" , выраженное Филиппом Вадлером:

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

То есть, расширение "вертикально" (добавление типов в иерархию) и "по горизонтали" (добавление функций, которые должны быть переопределены базовому классу) на программиста жестко.

В Reddit была длинная (как всегда) дискуссия, в которой я предложил решение на С++.

Это мост между OO (отлично подходит для добавления новых типов) и универсальным программированием (отлично подходит для добавления новых функций). Идея состоит в том, чтобы иметь иерархию чистых интерфейсов и набор неполиморфных типов. Свободные функции определяются по конкретным типам по мере необходимости, а мост с чистыми интерфейсами обеспечивается одним классом шаблона для каждого интерфейса (дополняется функцией шаблона для автоматического вывода).

Я нашел единственное ограничение на сегодняшний день: если функция возвращает интерфейс Base, она может быть сгенерирована как есть, даже если фактический тип wrapped поддерживает больше операций. Это типично для модульного дизайна (новые функции недоступны на сайте вызова). Я думаю, что это иллюстрирует чистый дизайн, однако я понимаю, что можно было бы "переделать" его на более подробный интерфейс. Go может, с поддержкой языка (в основном, интроспекция времени выполнения доступных методов). Я не хочу кодировать это в С++.


Как уже объяснял себя на reddit... Я просто воспроизведу и настрою код, который я уже отправил туда.

Итак, давайте начнем с двух типов и одной операции.

struct Square { double side; };
double area(Square const s);

struct Circle { double radius; };
double area(Circle const c);

Теперь давайте создадим интерфейс Shape:

class Shape {
public:
   virtual ~Shape();

   virtual double area() const = 0;

protected:
   Shape(Shape const&) {}
   Shape& operator=(Shape const&) { return *this; }
};

typedef std::unique_ptr<Shape> ShapePtr;

template <typename T>
class ShapeT: public Shape {
public:
   explicit ShapeT(T const t): _shape(t) {}

   virtual double area() const { return area(_shape); }

private:
  T _shape;
};

template <typename T>
ShapePtr newShape(T t) { return ShapePtr(new ShapeT<T>(t)); }

Хорошо, С++ является подробным. Немедленно проверьте использование:

double totalArea(std::vector<ShapePtr> const& shapes) {
   double total = 0.0;
   for (ShapePtr const& s: shapes) { total += s->area(); }
   return total;
}

int main() {
  std::vector<ShapePtr> shapes{ new_shape<Square>({5.0}), new_shape<Circle>({3.0}) };

  std::cout << totalArea(shapes) << "\n";
}

Итак, сначала упражнение, добавьте форму (да, все):

struct Rectangle { double length, height; };
double area(Rectangle const r);

Хорошо, пока все хорошо, добавьте новую функцию. У нас есть два варианта.

Во-первых, нужно изменить Shape, если оно в наших силах. Это совместимо с исходным кодом, но не совместимо с бинарными.

// 1. We need to extend Shape:
  virtual double perimeter() const = 0

// 2. And its adapter: ShapeT
  virtual double perimeter() const { return perimeter(_shape); }

// 3. And provide the method for each Shape (obviously)
double perimeter(Square const s);
double perimeter(Circle const c);
double perimeter(Rectangle const r);

Может показаться, что мы попадаем здесь в проблему выражения, но мы этого не делаем. Нам нужно было добавить периметр для каждого (уже известного) класса, потому что нет возможности автоматически его вывести; однако он не требовал редактирования каждого класса!

Следовательно, комбинация внешнего интерфейса и свободных функций позволяет нам аккуратно (ну, это С++...) обойти эту проблему.

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

Вторые варианты поэтому не навязчивы, ценой немного более многословной:

class ExtendedShape: public Shape {
public:
  virtual double perimeter() const = 0;
protected:
  ExtendedShape(ExtendedShape const&) {}
  ExtendedShape& operator=(ExtendedShape const&) { return *this; }
};

typedef std::unique_ptr<ExtendedShape> ExtendedShapePtr;

template <typename T>
class ExtendedShapeT: public ExtendedShape {
public:
   virtual double area() const { return area(_data); }
   virtual double perimeter() const { return perimeter(_data); }
private:
  T _data;
};

template <typename T>
ExtendedShapePtr newExtendedShape(T t) { return ExtendedShapePtr(new ExtendedShapeT<T>(t)); }

И затем определите функцию perimeter для всех тех Shape, которые мы хотели бы использовать с ExtendedShape.

Старый код, скомпилированный для работы с Shape, все еще работает. В любом случае, она не нуждается в новой функции.

Новый код может использовать новую функциональность и по-прежнему безболезненно взаимодействовать со старым кодом. (*)

Есть только одна небольшая проблема, если старый код возвращает ShapePtr, мы не знаем, имеет ли форма фактическая функция периметра (обратите внимание: если указатель создается внутри, он не был сгенерирован с помощью newExtendedShape). Это ограничение дизайна, упомянутого в начале. К сожалению:)

(*) Примечание: безболезненно подразумевается, что вы знаете, кто является владельцем. A std::unique_ptr<Derived>& и a std::unique_ptr<Base>& несовместимы, однако std::unique_ptr<Base> можно построить из std::unique_ptr<Derived> и a Base* из Derived*, поэтому убедитесь, что ваши функции чисты, и вы золотой.