Ищете лучший способ, чем виртуальное наследование в C++

Хорошо, у меня довольно сложная система на C++. Короче говоря, мне нужно добавить метод к стороннему абстрактному базовому классу. Третья сторона также предоставляет массу производных классов, которые также нуждаются в новой функциональности.

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

class Shape
{
    public:
        Shape(position);
        virtual ~Shape();

        virtual position GetPosition() const;
        virtual void SetPosition(position);

        virtual double GetPerimeter() const = 0;

    private: ...
};

class Square : public Shape
{
    public:
        Square(position, side_length);
    ...
};

class Circle, Rectangle, Hexagon, etc

Теперь вот моя проблема. Я хочу, чтобы класс Shape также включал функцию GetArea (). Кажется, мне просто нужно сделать:

class ImprovedShape : public virtual Shape
{
    virtual double GetArea() const = 0;
};

class ImprovedSquare : public Square, public ImprovedShape
{
    ...
}

А затем я делаю ImprovedSquare, унаследованный от ImprovedShape и Square. Как видите, я создал ужасный проблема наследования алмаза. Это было бы легко исправить, если бы сторонняя библиотека использовала виртуальное наследование для своих Square, Circle и т. д. Однако заставлять их делать это не является разумным вариантом.

Итак, что вы делаете, когда вам нужно добавить небольшую функциональность к интерфейсу, определенному в библиотеке? Есть хороший ответ?

Спасибо!

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
1 210
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Я полагаю, что шаблон фасад должен помочь.

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

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

Imbue 30.10.2008 17:00
Ответ принят как подходящий

У нас была очень похожая проблема в проекте, и мы решили ее, просто НЕ унаследовав ImprovedShape от Shape. Если вам нужна функциональность формы в ImprovedShape, вы можете использовать dynamic_cast, зная, что ваше приведение всегда будет работать. А в остальном как в вашем примере.

Я склоняюсь к этому методу. Я мог бы добавить к ImprovedShape метод GetShape (), который скрывает приведение. Я хотел бы, чтобы был способ проверки правильности во время компиляции.

Imbue 30.10.2008 16:58

Собственно, есть. Взгляните на Boost.Type_traits - и вы должны увидеть черту производного_from <>.

Dean Michael 31.10.2008 10:25

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

Gorpik 31.10.2008 11:30

Обойти проблему - не выход. Это исправление.

Dave Van den Eynde 31.10.2008 11:51

Почему этот класс должен происходить от формы?

class ImprovedShape : public virtual Shape
{
    virtual double GetArea() const = 0;
};

Почему бы просто не иметь

class ThingWithArea 
{
    virtual double GetArea() const = 0;
};

ImprovedSquare - это форма и ThingWithArea

В этом подходе есть проблема. Что, если мне нужно передать свой объект функции, которая должна вызывать GetPosition () и GetArea ()? Или что, если GetArea () на самом деле нужно полагаться на GetPerimeter ()?

Imbue 30.10.2008 16:47

GetArea () реализуется конкретным и поэтому может вызываться GetPerimeter ()

Dave Hillier 30.10.2008 19:04

Динамическое приведение можно использовать для преобразования в объект, который может вызывать GetArea ()

Dave Hillier 30.10.2008 19:05

В качестве альтернативы можно сделать так, чтобы эти функции принимали как Shape &, так и ThingWithArea &, и передавали ссылку на один и тот же объект для обоих аргументов на сайте вызова.

fizzer 31.10.2008 00:13

Вы хотели сказать «ImprovedSquare - это квадрат и ThingWithArea»?

Imbue 31.10.2008 19:19

Оглядываясь назад на это Q - ThingWithArea - дерьмовое имя. По определению это форма. Что еще имеет площадь, кроме формы? Я выбрал имя, но должен был использовать пространства имен.

Dave Hillier 02.01.2013 02:13

Возможно, вам следует прочитать правильное наследование и сделать вывод, что ImprovedShape не нужно наследовать от Shape, но вместо этого может использовать Shape для своих функций рисования, аналогично обсуждению в пункте 21.12 этого FAQ о том, как SortedList не должен наследовать от List, даже если он хочет обеспечить ту же функциональность, он может просто использовать a List.

Аналогичным образом ImprovedShape может использовать Shape, чтобы делать то, что она делает.

Возможно, использование шаблона декоратора? [http://en.wikipedia.org/wiki/Decorator_pattern[1]

Можно ли сделать совершенно другой подход - использовать шаблоны и методы метапрограммирования? Если вы не обязаны не использовать шаблоны, это может стать элегантным решением. Меняются только ImprovedShape и ImprovedSquare:

template <typename ShapePolicy>
class ImprovedShape : public ShapePolicy
{
public:
    virtual double GetArea();
    ImprovedShape(void);
    virtual ~ImprovedShape(void);

protected:
    ShapePolicy shape;
    //...
};

и ImprovedSquare становится:

class ImprovedSquare : public ImprovedShape<Square>
{
public:
    ImprovedSquare(void);
    ~ImprovedSquare(void);

    // ...

};

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

Странно называть класс тяжелого бетона «политикой» второстепенного класса расширения. Более обычным является называть ImprovedShape «миксином».

fizzer 31.10.2008 00:39

Если вам нужно вычислить площадь произвольной формы в функции, что вы передадите в функцию? Нет общего базового класса между ImprovedSquare и ImprovedCircle (кроме Shape). Каждая функция, использующая ImprovedShape, становится шаблоном. Никакой привязки к пробегу.

user3458 31.10.2008 01:34

fizzer - да, здесь лучше использовать миксин, но шаблон - это шаблон политики, реализованный Александреску (современный дизайн C++). Политика делает акцент на поведении, а это именно то, что здесь требуется. Недостающий элемент - это знание того, что отображается (или нет) из базовой библиотеки Shape.

twokats 31.10.2008 16:02

Я, должно быть, что-то упускаю. Почему ImprovedShape наследуется от ShapePolicy и владеет объектом ShapePolicy?

Imbue 31.10.2008 19:12

Imbue: краткий ответ заключается в том, что нам нужно поведение Shape и интерфейса. Обычно политики не являются автономными классами (поэтому fizzer прав, это скорее смешанный класс), но аналогичный пример может быть построен с настоящей политикой. (см. ссылку ниже)

twokats 03.11.2008 23:18

Вот ссылка: amazon.com/Modern-Design-Programming-Patterns-Depth/dp/… В главе 1 подробно рассказывается о стратегии, основанной на шаблоне политики.

twokats 03.11.2008 23:19

Подход Дэйва Хиллера правильный. Разделите GetArea() на отдельный интерфейс:

class ThingWithArea
{
public:
    virtual double GetArea() const = 0;
};

Если бы дизайнеры Shape поступили правильно и сделали интерфейс в чистом виде, а общедоступные интерфейсы конкретных классов были достаточно мощными, вы могли иметь экземпляры конкретных классов в качестве членов. Вот так получается SquareWithArea (ImprovedSquare - плохое название), будучи Shape и ThingWithArea:

class SquareWithArea : public Shape, public ThingWithArea
{
public:
    double GetPerimeter() const { return square.GetPerimeter(); }
    double GetArea() const { /* do stuff with square */ }

private:
    Square square;
};

К сожалению, разработчики Shape внедрили некоторую реализацию в Shape, и вы будет иметь две его копии на каждый SquareWithArea, как в алмаз, который вы изначально предложили.

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

class SquareWithArea : public Square, public ThingWithArea
{
};

В наши дни считается дурным тоном выводить нас из конкретных классов в C++. Трудно найти действительно хорошее объяснение, почему вам не следует этого делать. Обычно люди процитируйте статью 33 Мейерса «Более эффективный C++», в которой указывается на невозможность написать достойный operator=() среди прочего. Возможно, тогда вам следует никогда не делайте этого для классов с семантикой значений. Еще одна ловушка - это то, где конкретный класс не имеет виртуального деструктора (поэтому вам следует никогда не выводятся публично из контейнеров STL). Ни то, ни другое здесь не применимо. Постер кто снисходительно отправил вас на страницу часто задаваемых вопросов по C++, чтобы узнать о наследовании, неправильно - добавление GetArea() не нарушает заменяемость Лисков. О единственный риск, который я вижу, связан с переопределением виртуальных функций в конкретные классы, когда разработчик позже меняет имя и молча прерывает ваш код.

Подводя итог, я думаю, что вы можете получить от Square с чистой совестью. (В качестве утешения вам не придется писать все функции переадресации для интерфейс формы).

Теперь перейдем к проблеме функций, которым нужны оба интерфейса. Мне не нравится ненужные dynamic_cast. Вместо этого заставьте функцию использовать ссылки на оба интерфейса и передают ссылки на один и тот же объект для обоих на сайте вызова:

void PrintPerimeterAndArea(const Shape& s, const ThingWithArea& a)
{
    cout << s.GetPerimeter() << endl;
    cout << a.GetArea() << endl;
}

// ...

SquareWithArea swa;
PrintPerimeterAndArea(swa, swa);

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

Это подводит нас к единственному случаю, когда я бы подумал о передаче одной ссылки и получить другой с помощью dynamic_cast - где важно, чтобы два ссылки относятся к одному и тому же экземпляру объекта. Вот очень надуманный пример:

void hardcopy(const Shape& s, const ThingWithArea& a)
{
    Printer p;
    if (p.HasEnoughInk(a.GetArea()))
    {
        s.print(p);
    }
}

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

Еще один подход к метапрограммированию / миксину, на этот раз немного под влиянием трейтов. Предполагается, что вычисляемая площадь - это то, что вы хотите добавить на основе открытых свойств; вы могли бы сделать что-то, что сохранялось бы с инкапсуляцией, это цель, а не модуляризация. Но тогда вам нужно написать GetArea для каждого подтипа, а не использовать полиморфный, где это возможно. Стоит ли это того, зависит от того, насколько вы привержены инкапсуляции, и есть ли в вашей библиотеке базовые классы, которые вы можете использовать, например, Прямоугольная форма ниже.

#import <iostream>

using namespace std;

// base types
class Shape {
    public:
        Shape () {}
        virtual ~Shape () { }
        virtual void DoShapyStuff () const = 0;
};

class RectangularShape : public Shape {
    public:
        RectangularShape () { }

        virtual double GetHeight () const = 0 ;
        virtual double GetWidth  () const = 0 ;
};

class Square : public RectangularShape {
    public:
        Square () { }

        virtual void DoShapyStuff () const
        {
            cout << "I\'m a square." << endl;
        }

        virtual double GetHeight () const { return 10.0; }
        virtual double GetWidth  () const { return 10.0; }
};

class Rect : public RectangularShape {
    public:
        Rect () { }

        virtual void DoShapyStuff () const
        {
            cout << "I\'m a rectangle." << endl;
        }

        virtual double GetHeight () const { return 9.0; }
        virtual double GetWidth  () const { return 16.0; }
};

// extension has a cast to Shape rather than extending Shape
class HasArea {
    public:
        virtual double GetArea () const = 0;
        virtual Shape& AsShape () = 0;
        virtual const Shape& AsShape () const = 0;

        operator Shape& ()
        {
            return AsShape();
        }

        operator const Shape& () const
        {
            return AsShape();
        }
};

template<class S> struct AreaOf { };

// you have to have the declaration before the ShapeWithArea 
// template if you want to use polymorphic behaviour, which 
// is a bit clunky
static double GetArea (const RectangularShape& shape)
{
    return shape.GetWidth() * shape.GetHeight();
}

template <class S>
class ShapeWithArea : public S, public HasArea {
    public:
        virtual double GetArea () const
        {
            return ::GetArea(*this);
        }
        virtual Shape& AsShape ()             { return *this; }
        virtual const Shape& AsShape () const { return *this; }
};

// don't have to write two implementations of GetArea
// as we use the GetArea for the super type
typedef ShapeWithArea<Square> ImprovedSquare;
typedef ShapeWithArea<Rect> ImprovedRect;

void Demo (const HasArea& hasArea)
{
    const Shape& shape(hasArea);
    shape.DoShapyStuff();
    cout << "Area = " << hasArea.GetArea() << endl;
}

int main ()
{
    ImprovedSquare square;
    ImprovedRect   rect;

    Demo(square);
    Demo(rect);

    return 0;
}

+1 для умных. Однако OP, вероятно, будет трудно применить технику микширования к существующей иерархии, поскольку всем конкретным классам потребуется один и тот же интерфейс конструктора.

fizzer 02.11.2008 19:16

Моя ошибка - ShapeWithArea просто нужно реализовать все возможные конструкторы, и будет создан нужный.

fizzer 02.11.2008 19:35

Насколько я понял вопрос, решение вашей проблемы существует. Используйте addapter-pattern. Шаблон адаптера используется для добавить функциональность к определенному классу или обмениваться определенным поведением (т.е. методов). Учитывая нарисованный вами сценарий:

class ShapeWithArea : public Shape
{
 protected:
  Shape* shape_;

 public:
  virtual ~ShapeWithArea();

  virtual position GetPosition() const { return shape_->GetPosition(); }
  virtual void SetPosition(position)   { shape_->SetPosition(); }
  virtual double GetPerimeter() const  { return shape_->GetPerimeter(); }

  ShapeWithArea (Shape* shape) : shape_(shape) {}

  virtual double getArea (void) const = 0;
};

Шаблон-адаптер предназначен для адаптации поведения или функциональности класса. Вы можете использовать это для

  • изменить поведение класса, не перенаправляя, а повторно реализуя методы.
  • добавить поведение к классу, добавив методы.

Как это меняет поведение? Когда вы предоставляете объект типа base методу, вы также можете предоставить адаптированный класс. Объект будет вести себя так, как вы его проинструктировали, актор на объекте будет заботиться только об интерфейсе базового класса. Вы можете применить этот адаптер к любому производному от Shape.

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

Что-то типа:

template <class ShapeType, class AreaFunctor> 
int GetArea(const ShapeType& shape, AreaFunctor func);

Функции STL мин, Максимум можно рассматривать как аналогию для вашего случая. Вы можете найти минимальное и максимальное значение для массива / вектора объектов с учетом функции компаратора. Точно так же вы можете получить площадь любой заданной формы с помощью функции вычисления площади.

Другие вопросы по теме