И некоторые производные классы из этого, скажем, B, C, D... В 95% случаев я хочу пройти через все объекты и вызвать func или func2(), поэтому поэтому я их в вектор, например:
Однако в остальных 5% случаев я хочу сделать что-то другое для классов в зависимости от их подкласса. И я имею в виду совершенно разные, такие как функции вызова, которые принимают другие параметры или вообще не вызывают функции для некоторых подклассов. Я подумал о некоторых вариантах решения этого вопроса, никому из которых мне очень нравится:
Итак, может кто-нибудь предложить стиль/шаблон, который достигает того, что я хочу?
Ответ 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*
, поэтому убедитесь, что ваши функции чисты, и вы золотой.