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

时间:2008-10-30 07:34:40

标签: c++ inheritance multiple-inheritance

好吧,我在C ++中有一个有点复杂的系统。简而言之,我需要向第三方抽象基类添加方法。第三方还提供了大量的派生类,这些类也需要新的功能。

我正在使用一个提供标准Shape接口的库,以及一些常见的形状。

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
{
    ...
}

然后我去制作一个继承自ImprovedShape和Square的ImprovedSquare。好吧,正如你所看到的,我现在已经创建了可怕的diamond inheritance problem。如果第三方库使用virtual inheritance作为Square,Circle等,这很容易解决。但是,让他们这样做是不合理的选择。

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

谢谢!

10 个答案:

答案 0 :(得分:7)

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

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

为什么不拥有

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

ImprovedSquare是一个Shape,是一个ThingWithArea

答案 1 :(得分:4)

我认为facade模式应该可以解决问题。

将第三方界面包装到您自己的界面中,您的应用程序代码与包装界面而不是第三方界面一起使用。这样你就可以很好地隔离不受控制的第三方界面的变化。

答案 2 :(得分:4)

我们在一个项目中遇到了一个非常类似的问题,我们只是从Shape中派生出Im​​provedShape来解决它。如果你需要在ImprovedShape中使用Shape功能,你可以使用dynamic_cast,因为你知道你的演员阵容将永远有效。其余的就像你的例子一样。

答案 3 :(得分:4)

也许您应该阅读proper inheritance,并得出结论,ImprovedShape不需要从Shape继承,而是可以使用Shape作为其绘图功能,类似于关于SortedList的FAQ的第21.12节中的讨论即使它想要提供相同的功能,它也不必从List继承,它只需使用一个List。

以类似的方式,ImprovedShape可以使用一个Shape来做它的形状。

答案 4 :(得分:2)

答案 5 :(得分: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);

    // ...

};

您将避免钻石继承,从原始Shape(通过策略类)获取继承以及您想要的附加功能。

答案 6 :(得分:1)

Dave Hillier的方法是正确的。将GetArea()分隔到自己的界面中:

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

如果Shape的设计师做了正确的事情并使其成为纯粹的界面, 并且具体类的公共接口足够强大,你可以 将具体类的实例作为成员。这就是你得到的SquareWithAreaImprovedSquare名称很差)是ShapeThingWithArea

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 ++中的具体类派生它被认为是不好的形式。 很难找到一个非常好的解释为什么你不应该。通常,人 引用Meyers的更有效的C ++第33项,它指出了不可能性 写一篇体面的operator=()等等。那么,你应该这么做 永远不要为具有价值语义的类做这件事。另一个陷阱是 具体类没有虚拟析构函数(这就是你应该这样做的原因 永远不会公开源自STL容器)。这两个都不适用。海报 谁居高临下地送你到C ++ faq来了解继承是 错误 - 添加GetArea()并不违反Liskov的可替代性。关于 我能看到的唯一风险来自覆盖虚拟功能 具体的类,当实现者稍后更改名称并默默地中断时 你的代码。

总之,我认为你可以从良心中获得Square。 (作为安慰,您不必编写所有转发功能 Shape接口)。

现在针对需要两个接口的功能问题。我不喜欢 不必要的dynamic_cast。相反,让函数接受引用 两个接口并在调用站点传递对同一对象的引用:

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

// ...

SquareWithArea swa;
PrintPerimeterAndArea(swa, swa);

所有PrintPerimeterAndArea()需要做的工作是外围和a的来源 面积来源。这些碰巧实施并不是它的担忧 作为同一对象实例上的成员函数。可以想象,该地区可以 由它和Shape之间的一些数值积分引擎提供。

这让我们得到了唯一可以考虑传递一个参考的情况 并通过dynamic_cast获得另一个 - 两者的重要性 引用是指同一个对象实例。这是一个非常人为的例子:

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

即便如此,我可能更愿意发送两个引用而不是 dynamic_cast。我会依靠一个理智的整体系统设计来消除 两个不同实例的位的可能性被馈送到这样的函数。

答案 7 :(得分:1)

对元编程/ 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;
}

答案 8 :(得分:1)

GetArea()不必是成员。它可以是模板化函数,因此您可以为任何形状调用它。

类似的东西:

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

可以将STL minmax函数视为您案例的类比。给定比较器函数,您可以找到对象的数组/向量的最小值和最大值。同样明智的是,你可以得出任何给定形状的面积,只要有计算面积的函数。

答案 9 :(得分:0)

正如我所理解的那样,你的问题存在解决方案。使用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;
};

Adapter-Pattern旨在调整类的行为或功能。你可以用它来

  • 通过不转发但重新实现方法来更改类的行为。
  • 通过添加方法向类中添加行为。

它如何改变行为?当您向方法提供类型为base的对象时,您也可以提供适应的类。对象将按照您的指示运行,对象上的actor只关心基类的接口。 您可以将此适配器应用于Shape的任何派生。