C ++ Double Dispatch for Equals()

时间:2011-09-12 20:16:49

标签: c++ oop inheritance double-dispatch

想象一下,我有abstract base class Shape,派生类CircleRectangle

class Shape {};
class Circle : public Shape {};
class Rectangle : public Shape {};

我需要确定两个形状是否相等,假设我有两个Shape*指针。 (这是因为我有vector<Shape*>的两个实例,我想知道它们是否具有相同的形状。)

建议的方法是double dispatch。我想出的就是这个(这里大大简化了,所以形状等于同一类型的所有其他形状):

class Shape {
public:
    virtual bool equals(Shape* other_shape) = 0;
protected:
    virtual bool is_equal(Circle& circle) { return false; };
    virtual bool is_equal(Rectangle& rect) { return false; };
    friend class Circle;    // so Rectangle::equals can access Circle::is_equal
    friend class Rectangle; // and vice versa
};

class Circle : public Shape {
public:
    virtual bool equals(Shape* other_shape) { return other_shape->is_equal(*this); };
protected:
    virtual bool is_equal(Circle& circle) { return true; };
};

class Rectangle : public Shape {
public:
    virtual bool equals(Shape* other_shape) { return other_shape->is_equal(*this); };
protected:
    virtual bool is_equal(Rectangle& circle) { return true; };
};

这样可行,但我必须在equals中为每个派生类添加单独的friend函数和Shape声明。然后我必须将完全相同的equals函数复制粘贴到每个派生类中。这是一个非常多的样板,比如10种不同的形状!

有更简单的方法吗?

dynamic_cast是不可能的;太慢了。 (是的,我对它进行了基准测试。速度在我的应用程序中很重要。)

我尝试了这个,但它不起作用:

class Shape {
public:
    virtual bool equals(Shape* other_shape) = 0;
private:
    virtual bool is_equal(Shape& circle) { return false; };
};

class Circle : public Shape {
public:
    virtual bool equals(Shape* other_shape) { return other_shape->is_equal(*this); };
private:
    virtual bool is_equal(Circle& circle) { return true; };
};

class Rectangle : public Shape {
public:
    virtual bool equals(Shape* other_shape) { return other_shape->is_equal(*this); };
private:
    virtual bool is_equal(Rectangle& circle) { return true; };
};

equals()总是返回false,即使在相同的形状上也是如此。似乎调度总是选择is_equal(Shape&)基函数,即使可以使用“更具体”的匹配。这可能是有道理的,但我不太了解C ++调度,知道原因。

6 个答案:

答案 0 :(得分:5)

创建这样的方法时:

virtual bool is_equal(Shape& circle) { return false; };

在子类中,

virtual bool is_equal(Circle& circle) { return true; };

这些方法不一样。你有两个独立的虚方法,它们都没有被覆盖(它们重载甚至没有超载,正如Ben Voigt指出的那样)。当您致电Shape::is_equal时,只有一个版本:Shape::is_equal(Shape&) ...未被覆盖且始终返回false。

您必须在父类中定义单独的重载方法,然后在子类中重写它们。例如,

class Shape {
    // Choice between these two methods happens at compile time...
    virtual bool is_equal(Circle& circle) { return false; };
    virtual bool is_equal(Rectangle& circle) { return false; };
};

class Rectangle : Shape {
    // Choice between this and Shape::is_equal(Rectangle&) happens at runtime...
    virtual bool is_equal(Rectangle& circle) { return true; };
};

然而,使用这样的技巧,你可能无法接近C程序员的表现或简单性:

typedef enum {
    SHAPE_CIRCLE,
    SHAPE_RECTANGLE
} shape_type_t;

struct shape {
    shape_type_t type;
};

struct circle {
    shape_type_t type;
    ...
};

struct rectangle {
    shape_type_t type;
    ...
};

bool shape_equal(struct shape *x, struct shape *y)
{
    if (x->type != y->type)
        return false;
    switch (x->type) {
    case SHAPE_CIRCLE:
        return circle_equal((struct circle *) x, (struct circle *) y);
    case SHAPE_RECTANGLE:
        ...;
    }
}

如果重载和虚拟方法使您的代码比C版本更复杂,那么您可能希望重新考虑是否通过重载和虚拟方法解决了这个特定问题。

答案 1 :(得分:5)

双重调度已经得到很好的研究。双重调度的推广称为“多方法”。

Modern C++ Design的第11章详细解决了这个问题。您所描述的使用dynamic_cast<>的方法在第11.3节“Double Switch-on-Type:Brute Force”中。作者甚至描述了如何自动化大部分工作并自动生成对称重载。然后,作者介绍了基于std::map<>std::type_info的对数调度。最后,该部分以“Constant-Time Multimethods:Raw Speed”结尾,它(大致)基于回调函数矩阵。

所提出的解决方案包括处理仿函数和强制转换的冗长解释,以避免在存在多个(和虚拟)继承的情况下出现令人讨厌的陷阱。

如果考虑在C ++中实现多方法,我强烈建议您阅读本书并实施建议的解决方案。

答案 2 :(得分:1)

如果dynamic_cast太慢,你可以使用类型枚举和静态转换......

enum ShapeType
{
    SHAPE_TYPE_CIRCLE,
    SHAPE_TYPE_RECTANGLE
};

struct Shape
{
    virtual ShapeType GetShapeType() const = 0;
    virtual bool isEqual(const Shape& other) const = 0;
};

struct Circle : Shape
{
    virtual ShapeType GetShapeType() const { return SHAPE_TYPE_CIRCLE; }

    virtual bool isEqual(const Shape& other) const
    {
        if (other.GetShapeType() == SHAPE_TYPE_CIRCLE)
        {
            const Circle *circle = static_cast<const Circle*>(&other);

            // do some circle specific comparison
            return true;
        }
        return false;
    }
};

答案 3 :(得分:0)

我通常会引用dynamic_cast和虚函数。如果编译器不是太笨,那么动态转换一步与在vtable中进行两次跳转没有什么不同。

class shape
{
protected:
   virtual bool is_equal(const shape* s) const=0;
   friend bool oeprator==(const shape& a, cost shape& b)
   { return a.is_equal(&b); }
};

class circle: public shape
{
    double radius;
    point<duouble> center;
protected:
    virtual bool is_equal(const shape* s) const
    {
        const circle* p = dynamic_cast<const circle*>(s);
        return p && p->radius==radius && p->center==center;
    }
};

矩形或其他形状相同。 基本上,双重调度需要 - 如果N是classees,N 2 函数。 这样,你只需要N个函数(每个类一个)。

如果你认为动态转换太慢,你可以使用在基类中声明的枚举, 并由派生类正确初始化。 但是这需要您在每次添加新类时更新枚举值。 例如:     阶级形状     {     保护:        enum shapes_type {no_shape,circle_shape,rectangle_shape};        shapes_type my_type;        virtual bool is_equal(const shape * s)const = 0;        朋友bool oeprator ==(const shape&amp; a,cost shape&amp; b)        {return a.is_equal(&amp; b); }        shape():my_type(no_shape)        {}     };

class circle: public shape
{
    double radius;
    point<duouble> center;
protected:
    virtual bool is_equal(const shape* s) const
    {
        const circle* p = static_cast<const circle*>(s);
        return my_type == s->my_type && p->radius==radius && p->center==center;
    }
public:
    circle() { my_type = circle_shape; }
};

如果依赖于base_defined枚举是不可接受的(未知的可能类的数量),你可以依赖一个简单的值(例如:一个整数),它可以单义地表示一个带有类似技巧的类型:

int int_generator()
{ static int x=0; return ++x; }

template<class T>
int  id_for_type()
{ static int z = int_generator(); return z; }

class shape
{
...
int my_type;
};


class circle
{
...
   circle() { my_type = id_for_type<circle>(); }
};

答案 4 :(得分:0)

虚拟函数可以轻松替换dynamic_cast RTTI类型检查,如下所示:http://ideone.com/l7Jr5

struct Shape
{
    struct subtype { enum { Shape, Circle, Rectangle, ColoredCircle }; };

    virtual bool is_a( int type ) const { return type == subtype::Shape; }
    virtual bool is_equal(const Shape& s) const { return false; }
};

struct Rectangle : Shape
{
    virtual bool is_a( int type ) const { return type == subtype::Rectangle || Shape::is_a(type); }
    virtual bool is_equal(const Shape& s) const
    {
        if (!s.is_a(subtype::Rectangle)) return false;
        const Rectangle& r = static_cast<const Rectangle&>(s);
        return true; // or check width and height
    }
};

struct Circle : Shape
{
    virtual bool is_a( int type ) const { return type == subtype::Circle || Shape::is_a(type); }
    virtual bool is_equal(const Shape& s) const
    {
        if (!s.is_a(subtype::Circle)) return false;
        const Circle& c = static_cast<const Circle&>(s);
        return true; // or check radius
    }
};

struct ColoredCircle : Circle
{
    virtual bool is_a( int type ) const { return type == subtype::ColoredCircle || Circle::is_a(type); }
};

int main(void)
{
    Rectangle x;
    Shape y;
    return x.is_equal(y);
}

-

为什么有10个“完全相同”的功能? Rectangle::is_equal(const Rectangle&) const不应该比较特定于矩形的成员吗?

如果所有矩形都属于单个等价类,就像您展示的代码一样,那么您可以只使用一个返回等价类的虚函数。

答案 5 :(得分:0)

在我的设计中,我将Shape::operator==方法移至私有而不实现它。正确解决这个问题的工作量不值得。

换句话说,给定Shape *

的向量
std::vector<Shape *> my_shapes;

我可以做以下事情:

my_shapes.push_back(new Rectangle);
my_shapes.push_back(new Circle);

比较对象时会出现问题:

Shape * p_shape_1 = my_shapes[0];
Shape * p_shape_2 = my_shapes[1];
if (*p_shape_1 == *p_shape_2) {...}

表达式相当于:

p_shape_1-&GT;运算符==(* p_shape_2);

如果有虚拟或多态操作,则变为:

矩形::运算符==((环的));

换句话说,矩形很可能将自身与圆形或其他形状进行比较;无效的比较。

因此,在我的设计中,我禁止基于基类指针的相等比较。使用指向基类的指针可以比较的唯一内容是基类中的内容。