在C++中寻找比虚拟继承更好的方法

Looking for a better way than virtual inheritance in C++

本文关键字:继承 更好 方法 虚拟 C++ 寻找      更新时间:2023-10-16

好吧,我在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
{
    ...
}

然后我去做一个改进的正方形,它继承了改进的形状和正方形。好吧,正如你所看到的,我现在已经创建了可怕的钻石继承问题。如果第三方库对其正方形、圆形等使用虚拟继承,则很容易解决此问题。但是,让他们这样做不是一个合理的选择。

那么,当您需要向库中定义的接口添加一些功能时,您会怎么做?有好的答案吗?

谢谢!

为什么这个类需要从形状派生?

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

为什么不干脆有

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

改进的正方形是一个形状,是一个ThingWithArea

我们在一个项目中遇到了一个非常相似的问题,我们通过不从 Shape 派生 BetterShape 来解决它。 如果您需要改进形状中的形状功能,您可以dynamic_cast,因为您知道您的强制转换将始终有效。 其余的就像您的示例一样。

我想立面图案应该可以解决问题。

将第三方接口包装

到您自己的接口中,应用程序的代码将使用包装器接口而不是第三方接口。这样,您也可以很好地隔离不受控制的第三方界面中的更改。

也许您应该阅读正确的继承,并得出结论,ImprovedShape 不需要从 Shape 继承,而是可以使用 Shape 来实现其绘图功能,类似于第 21.12 点中的讨论 常见问题解答 关于 SortedList 如何不必从列表继承,即使它想要提供相同的功能, 它可以简单地使用列表。

以类似的方式,ImprovedShape可以使用Shape来执行其Shape操作。

可能是装饰器模式的用途?[http://en.wikipedia.org/wiki/Decorator_pattern][1]

是否有可能做一种完全不同的方法 - 使用模板和元编程技术? 如果您不限于不使用模板,这可以提供一个优雅的解决方案。 只有ImprovedShapeImprovedSquare更改:

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);
    // ...
};

您将避免菱形继承,同时从原始形状(通过策略类(获取继承以及所需的附加功能。

另一种元编程/mixin,这次有点受特质的影响。它假定计算面积是您要根据公开的属性添加的内容;你可以做一些与封装保持一致的事情,这是一个目标,而不是模块化。 但是,您必须为每个子类型编写一个 GetArea,而不是在可能的情况下使用多态子类型。这是否值得取决于你对封装的承诺程度,以及你的库中是否有可以利用常见行为的基类,比如下面的 RectangularShape。

#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;
}

Dave Hillier的方法是正确的。 将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++的具体类中衍生出来被认为是不好的形式。很难找到一个真正好的解释为什么你不应该。通常,人们引用迈耶斯更有效C++第33项,指出不可能除其他外,写一篇体面的operator=()。 那么,也许你应该永远不要对具有值语义的类这样做。 另一个陷阱是具体类没有虚拟析构函数(这就是为什么你应该永远不要公开派生自 STL 容器(。 这里都不适用于此。 海报谁居高临下地把你送到C++常见问题解答以了解继承是错误 - 添加GetArea()并不违反 Liskov 可替代性。 大约我能看到的唯一风险来自覆盖具体类,当实现者稍后更改名称并静默中断时您的代码。

总之,我认为你可以问心无愧地从Square得出。(作为安慰,您不必编写所有转发函数形状接口(。

现在谈谈需要两个接口的函数的问题。 我不喜欢不必要的dynamic_cast s。 相反,使函数引用在调用站点上,两个接口和传递对同一对象的引用:

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(( 不必是成员。它可以是模板化函数,以便您可以为任何形状调用它。

像这样:

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

STL 最小值、最大值函数可以看作是您情况的类比。您可以找到给定比较器函数的对象数组/向量的最小值和最大值。同样明智的是,您可以导出任何给定形状的面积,只要函数计算面积。

正如我所理解的那样,您的问题有一个解决方案。使用加法模式。适配器模式用于向特定类添加功能或交换特定行为(即方法(。考虑你描绘的场景:

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 的任何派生。