c++ for Equals()的双分派

C++ Double Dispatch for Equals()

本文关键字:分派 for Equals c++      更新时间:2023-10-16

假设我有抽象基类Shape,派生类CircleRectangle

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

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

推荐的方法是双重调度。我的想法是这样的(这里进行了极大的简化,这样形状就等于所有其他相同类型的形状):

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

这是有效的,但是我必须在Shape中为每个派生类添加单独的equals函数和friend声明。然后,我还必须将完全相同的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++分派,不知道为什么。

当您创建这样的方法时:

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版本更复杂,那么你可能希望重新考虑是否用重载和虚方法解决了这个特殊的问题。

双重调度已经得到了很好的研究。双调度的泛化称为"多方法"。

现代c++设计的第11章详细讨论了这个问题。使用dynamic_cast<>的方法,你描述的是在第11.3节"双开关类型:蛮力"。作者甚至描述了如何自动化大部分工作并自动生成对称过载。然后介绍了一种基于std::map<>std::type_info的对数调度方法。最后,本节以"恒时多方法:原始速度"结束,该方法(大致)基于回调函数矩阵。

给出的解决方案包括对处理函子和强制类型转换的冗长解释,以避免存在多重(和虚拟)继承时的严重缺陷。

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

如果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;
    }
};

虚函数可以很容易地取代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不应该比较特定于矩形的成员吗?

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

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

换句话说,给定一个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 ->操作符= = (* p_shape_2);

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

矩形::操作符= = ((圆);

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

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

我通常指的是dynamic_cast和虚函数。如果编译器不是太笨的话,动态强制转换一步和在虚函数表中进行两次跳转并没有太大的区别。

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个是类,那么N2个函数。这样,你只需要N个函数(每个类一个)。

如果您觉得动态强制转换太慢,可以使用在基类中声明的enum,并由派生类正确初始化。但这需要您在每次添加新类时更新枚举值。例如:类的形状{保护:Enum shapes_type {no_shape, circle_shape, rectangle_shape};shapes_type my_type;虚拟bool is_equal(const shape* s) const=0;朋友bool操作符==(const shape&A、成本形态b){返回a.s is_equal(&b);}形状():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的enum(不知道有多少可能的类),您可以依赖于一个简单的值(例如:整数),它可以用如下技巧惟一地表示类型:

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