在没有RTTI的情况下比较C ++中的多态基类型

时间:2012-07-04 15:38:24

标签: c++ polymorphism

我有一些基本类型的Shape指针。我想使用==运算符比较这些对象。如果对象具有不同的派生类型,则==运算符显然应返回false。如果它们具有相同的派生类型,则应该比较派生类型的成员。

我已经读过,使用C ++ RTTI是不好的做法,只能在极少数和必要的情况下使用。据我所知,如果不使用RTTI,通常无法解决这个问题。每个重载的==运算符都必须检查typeid,如果它们相同,则执行dynamic_cast并比较成员。这似乎是一种常见的需求。这个问题有某种成语吗?

#include <iostream>
using namespace std;

class Shape {
  public:
    Shape() {}
    virtual ~Shape() {}
    virtual void draw() = 0;

    virtual bool operator == (const Shape &Other) const = 0;
};

class Circle : public Shape {
  public:
    Circle() {}
    virtual ~Circle() {}
    virtual void draw() { cout << "Circle"; }

    virtual bool operator == (const Shape &Other) const {
      // If Shape is a Circle then compare radii
    }

  private:
    int radius;
};

class Rectangle : public Shape {
  public:
    Rectangle() {}
    virtual ~Rectangle() {}
    virtual void draw() { cout << "Rectangle"; }

    virtual bool operator == (const Shape &Other) const {
      // If Shape is a Rectangle then compare width and height
    }

  private:
    int width;
    int height;
};

int main() {
  Circle circle;
  Rectangle rectangle;

  Shape *Shape1 = &circle;
  Shape *Shape2 = &rectangle;

  (*Shape1) == (*Shape2); // Calls Circle ==
  (*Shape2) == (*Shape1); // Calls Rectangle ==
}

4 个答案:

答案 0 :(得分:10)

使用RTTI。 使用typeid,但使用static_cast而不是dynamic_cast

从设计的角度来看,我认为这正是RTTI的用途,任何替代解决方案都必然会更加丑陋。

virtual bool operator == (const Shape &Other) const {
    if(typeid(Other) == typeid(*this))
    {
        const Circle& other = static_cast<const Circle&>(Other);
        // ...
    }
    else
        return false;
}

从表现的角度来看: typeid往往很便宜,只是简单地查找存储在虚拟表中的指针。您可以廉价地比较动态类型的相等性。

然后,一旦您知道自己拥有合适的类型,就可以安全地使用static_cast

dynamic_cast有一个缓慢的声誉(就像“与虚函数调用相比”一样慢,不像“与java中的演员相比”那么慢),因为它也会分析类用于处理继承的层次结构(以及多重继承)。你不需要在这里处理。

答案 1 :(得分:4)

当然可以在不使用typeid和投射的情况下完成。但它有点麻烦所以你必须决定它是否值得做。

第一版 - 双人访客

使用visitor pattern

class ShapeVisitor
{
public:
    virtual void visitCircle(Circle const &) = 0;
    virtual void visitRectangle(Rectangle const &) = 0;
    // other shapes
}

要上课Shape添加

virtual void acceptVisitor(ShapeVisitor &) = 0;

访客

class CircleComparingVisitor : public ShapeVisitor
{
    Circle const & lhs; // shorthand for left hand side
    bool equal; // result of comparison
public:
    CircleComparingVisitor(Circle const & circle):lhs(circle), equal(false){}
    virtual void visitCircle(Circle const & rhs) {equal = lhs.radius == rhs.radius;}
    virtual void visitRectangle(Rectangle const &) {}
    // other shapes
    bool isEqual() const {return equal;}
}
// other shapes analogically

class ShapeComparingVisitor
{
    Shape const & rhs; // right hand side
    bool equal;
public:
    ShapeComparingVisitor(Shape const & rhs):rhs(rhs), equal(false) {}

    bool isEqual() const {return equal;}

    virtual void visitCircle(Circle const & lhs)
    {
        CircleComparingVisitor visitor(lhs);
        rhs.accept(visitor);
        equal = visitor.isEqual();
    }
    virtual void visitRectangle(Rectangle const & lhs)
    {
        RectangleComparingVisitor visitor(lhs);
        rhs.accept(visitor);
        equal = visitor.isEqual();
    }
}

最后operator==无需虚拟

bool Shape::operator==(const Shape &rhs) const
{
    ShapeComparingVisitor visitor(rhs);
    this->accept(visitor);
    return visitor->isEqual();
}

第二个想法 - operator==可能是虚拟的并使用适当的比较访问者 - 因此您可以摆脱ShapeComparingVisitor

第二版 - 双重调度

您添加到Shape

virtual bool compareToCircle(Circle const &) const == 0;
virtual bool compareToRectangle(Rectangle const &) const == 0;

以特定形状实施

现在举例来说

bool Circle::operator==(Shape const & rhs) const
{
    return rhs.compareToCircle(*this);
}

答案 2 :(得分:1)

这正是RTTI的用途。在编译时,你只知道它是Shape&,所以你只需要进行运行时检查,看看它实际上是什么派生类型,然后才能进行有意义的比较。在不违反多态性的情况下,我不知道有任何其他方法可以做到这一点。

你可以为operator ==为不同的派生类型组合定义许多自由函数,但它不会有多态行为,因为你可能通过Shape&指针处理这些函数,所以即使调用代码也没有实际上知道对象的类型。

因此,RTTI在这里(几乎)是不可避免的,事实上这种情况正是RTTI存在的原因。在某些情况下,它只被认为是不好的做法,因为它增加了一定的脆弱性(当事情不属于你知道如何处理的类型时,你必须确保处理,因为任何人都可以出现并创建{{的新子类1}}),它增加了运行时成本。但是,您已经通过使用虚拟方法支付了运行时成本。

我说'几乎不可避免',因为你可能会编造一些系统,该系统对传递给Shape的对象进行进一步的虚方法调用,以获得正确的比较行为,但实际上是另一种虚方法查找(记住,虚拟方法也有运行时性能损失,因为编译器不知道将调用哪个实现,因此无法放入具体的函数地址)可能不会比RTTI的成本更快。

如果有人知道如何在没有这笔费用的情况下做到这一点,我很乐意看到它。

答案 3 :(得分:1)

我的感觉是,当你正在深入研究对象的内部表征时,存在对Liskov替换原则的根本违反。但是,如果您很乐意公开对象的内部表示(或者由于其他原因必须这样做),那么这样的事情就可以了。

class Shape
{
   virtual void std::string serialize() const =0;
   bool operator==( const Shape & s )
   {
      return this.serialize() == s.serialize();
   }
};