7

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

4 回答 4

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 中的强制转换相比”慢),因为它还将分析类层次结构以处理继承(以及多个继承,也是)。你不需要在这里处理它。

于 2012-07-05T10:34:57.020 回答
4

当然,它可以在不使用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);
}
于 2012-07-05T10:14:56.850 回答
1

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

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

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

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

如果有人知道一种完全不用花费成本的方法,我很乐意看到它。

于 2012-07-04T15:51:14.207 回答
1

我的感觉是,当您深入研究对象的内部表示时,这里根本违反了 Liskov 替换原则。但是,如果您乐于公开对象的内部表示(或者出于其他原因必须这样做),那么这样的事情就会起作用。

class Shape
{
   virtual void std::string serialize() const =0;
   bool operator==( const Shape & s )
   {
      return this.serialize() == s.serialize();
   }
};
于 2012-08-24T04:17:31.720 回答