代码之家  ›  专栏  ›  技术社区  ›  Imbue

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

  •  4
  • Imbue  · 技术社区  · 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
    {
        ...
    }
    

    然后我去做一个从改进的形状和正方形继承的改进的正方形。嗯,正如你所看到的,我现在创造了恐惧 diamond inheritance problem . 如果第三方库使用 virtual inheritance 但是,对于他们的正方形、圆形等,让他们这样做不是一个合理的选择。

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

    谢谢!

    10 回复  |  直到 14 年前
        1
  •  4
  •   Gorpik    16 年前

    我们在一个项目中遇到了一个非常相似的问题,我们通过不从形状中派生改进的形状来解决它。如果您需要改进形状的形状功能,您可以动态铸造,知道您的铸造将始终工作。剩下的就跟你的例子一样。

        2
  •  7
  •   Dave Hillier    16 年前

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

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

    为什么不

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

    改进的正方形是一个形状,是一个很薄的区域

        3
  •  4
  •   andreas buykx    16 年前

    我想 facade 模式应该起作用。

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

        4
  •  4
  •   Dave Van den Eynde    16 年前

    也许你应该读一下 proper inheritance 并得出结论,改进后的形状不需要从形状继承,而是可以将形状用于其绘图功能,类似于第21.12点关于排序列表如何不必从列表继承的常见问题解答中的讨论,即使它希望提供相同的功能,也可以简单地 使用 一览表。

    以类似的方式,改进的形状可以 使用 一个形状来做它的形状的事情。

        5
  •  2
  •   Binary Worrier    16 年前
        6
  •  1
  •   twokats    16 年前

    有没有可能采用完全不同的方法——使用模板和元编程技术?如果您不局限于不使用模板,这可以提供一个优雅的解决方案。只有 ImprovedShape ImprovedSquare 变化:

    template <typename ShapePolicy>
    class ImprovedShape : public ShapePolicy
    {
    public:
        virtual double GetArea();
        ImprovedShape(void);
        virtual ~ImprovedShape(void);
    
    protected:
        ShapePolicy shape;
        //...
    };
    

    以及 改进广场 变成:

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

    您将避免菱形继承,既可以从原始形状(通过策略类)继承,也可以获得所需的附加功能。

        7
  •  1
  •   community wiki Pete Kirkham    16 年前

    另一种是元编程/混合,这次有点受特性的影响。 它假定计算区域是您想要基于公开属性添加的;您可以做一些与封装保持一致的事情,这是一个目标,而不是模块化。但是,您必须为每个子类型编写一个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;
    }
    
        8
  •  1
  •   fizzer    16 年前

    戴夫·希利尔的方法是正确的。分开 GetArea() 进入自己的界面:

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

    如果形状的设计者做了正确的事情,使它成为一个纯粹的界面, 具体类的公共接口足够强大,您可以 将具体类的实例作为成员。你就是这样得到的 SquareWithArea ( ImprovedSquare 是个糟糕的名字) Shape 和A ThingWithArea :

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

    不幸的是, 形状 设计人员将一些实现 形状 和你 最后会随身携带两份 方形区域 ,就像在 你最初提议的钻石。

    这几乎迫使你进入最紧密的耦合,因此最少 理想,解决方案:

    class SquareWithArea : public Square, public ThingWithArea
    {
    };
    

    现在,从C++中的具体类中派生是一种不好的形式。 很难找到一个很好的解释为什么你不应该这样做。通常,人们 引用迈尔斯更有效的C++项目33,指出了不可能。 写一篇像样的 operator=() 除此之外。也许,那么,你应该 对于具有值语义的类永远不要这样做。另一个陷阱是 具体类没有虚拟析构函数(这就是为什么您应该 不要公开地从STL容器派生)。这里也不适用。海报 谁屈尊把你送到C++ FAQ去学习继承 错误添加 GETRAIAL() 不违反Liskov可替代性。关于 我能看到的唯一风险来自重写 具体类,当实现者稍后更改名称并静默中断时 您的代码。

    总的来说,我认为你可以从一个问心无愧的广场上得到。 (作为安慰,您不必为 形状界面)。

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

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

    所有 PrintPerimeterAndArea() 需要做的工作是一个周界和 区域来源。这些措施的实施并不令人担忧。 作为同一对象实例上的成员函数。可以想象,这个地区可以 由数字积分引擎提供。 形状 .

    这使我们看到了唯一一个我会考虑传递一个引用的情况。 让另一个通过 动态铸件 -重要的是, 引用指向同一对象实例。这里有一个非常人为的例子:

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

    即便如此,我还是宁愿寄两份推荐信而不是 动态铸件 . 我将依靠一个健全的整体系统设计来消除 两个不同实例的位被送入这样的函数的可能性。

        9
  •  1
  •   yasouser    14 年前

    getArea()不需要是成员。它可以是模板化函数,这样您就可以为任何形状调用它。

    类似:

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

    STL min , max 函数可以被看作是您案例的一个类比。对于给定比较器函数的对象数组/向量,可以找到最小值和最大值。和wise一样,如果函数用于计算面积,则可以导出任何给定形状的面积。

        10
  •  0
  •   mstrobl    16 年前

    正如我理解的那样,你的问题有一个解决办法。使用 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的对象时,还可以提供适应的类。对象将按照您指示的方式工作,对象上的参与者只关心基类的接口。 您可以将此适配器应用于任何形状派生。