比较没有RTTI的C++中的多态碱基类型

Comparing Polymorphic Base Types in C++ without RTTI

本文关键字:多态 基类 类型 C++ RTTI 比较      更新时间:2023-10-16

我有一些指向基本类型的 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 ==
}

使用 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 中的强制转换相比"那样慢),因为它还会分析类层次结构来处理继承(以及多重继承)。你不需要在这里处理这个问题。

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

版本一 - 双重访客

使用访客模式

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

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

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

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

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

如果有人知道一种无需成本的方法,我很想看到它。

我的感觉是,这里从根本上违反了Liskov替换原则,因为你正在挖掘对象的内部表示。但是,如果您乐于公开对象的内部表示形式(或者出于其他原因必须这样做),那么这样的事情将起作用。

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