使此代码正确多态

Making this code properly polymorphic

我有一个抽象的 class 父级,它有多个子级和用于与每个子级交互的空白函数。每个 Child 都会覆盖 Parent 的功能,并以不同的方式与其他 Childs 交互;即 Child1 对 interact_with(Child1)、interact_with(Child2) 等有不同的实现

在 Parent 中,我有一个函数 interact_with(Parent foo)。每个希望与另一个 Child 交互的 Child 都必须首先通过此功能。到目前为止一切都很好,但是我们 运行 遇到了一个问题:在处理了一些基本逻辑之后,Child 需要知道其参数的具体类型,以便它可以继续调用自己的重写函数。目前我有这个:

Child1* child1 = dynamic_cast<Child1*>(foo);
Child2* child2 = dynamic_cast<Child2*>(foo);
Child3* child3 = dynamic_cast<Child3*>(foo);

if(child1 != nullptr){
    interact_with(child1)
}

else if(child2 != nullptr){
    interact_with(child2)
}

else if(child3 != nullptr){
    interact_with(child3)
}

有效,但这不是一个很好的解决方案。当我有这么多 class 时,情况会变得特别糟糕。这是否表明基础设计有缺陷,如果是,我将如何改进它?

编辑:澄清一下:我有这样的东西

//Parent is an abstract class
class Parent
{
    void interact_with(Parent* foo){
        //this is here because there is a lengthy code segment
        //that needs to be run no matter what child interacts
        //with which

        //afterwards, I need to interact with whatever foo really is
    }

    virtual void interact_with(Child1* child){*blank*};
    virtual void interact_with(Child2* child){*blank*};
    virtual void interact_with(Child3) child){*blank*};
};

class Child1 : public Parent
{
    virtual void interact_with(Child1* child){*do something*};
    virtual void interact_with(Child2* child){*do something else*};
    virtual void interact_with(Child3* child){*do something else*};
};

已经。

像你说的那样使用 dynamic_cast<> 是糟糕的设计。您应该在声明中像这样使函数 interact_with 虚拟。

virtual void interact_with(Parent foo);

这将使方法调用使用子 class 的 interact_with 实现而不是父 class 的实现。然后你可以用这个替换你写的所有东西。

interact_with(foo);

Here is a pretty good explanation of virtual methods.

你要的是double dispatch

根据您的要求,您可以采用多种方法。一个非常通用的方法是使用 Key(type, type) -> Value(function) 形式的简单映射,将参数类型对与要调用的函数相关联。

在这种情况下,您将需要一组 void function(Parent*, Parent*) 类型的自由函数(一个用于您需要的每个组合)和一个 std::unordered_map<std::pair<TypeId, TypeId>, FunctionType> 类型的映射,其中 TypeId 是具有值语义的某种形式的类型标识符。

然后你在运行时进行调度:

if(map_.find(make_pair(type1, type2)) != map_.end())
    map_[make_pair(type1, type2)](obj1, obj2);

不是在注册每个函数之前:

map_[make_pair(type1, type2)] = func12;
map_[make_pair(type2, type3)] = func23;
....

警告:此解决方案会破坏您的界面。不过别担心,它们已经坏了 ;)

在我看来,您的设计错误如下:尽管所有 children 都是 "equal",但您选择了其中一个负责交互并在其上调用一个方法(如果您想要 3, 4, ..., N等于children(在一个数组中)同时交互,谁负责?)

如果在您的应用程序中所有 object 都同等重要并且没有 object 负责交互,您应该将交互移动到自由重载的二进制函数中:

void interact(Child1* a, Child1* b);

void interact(Child1* a, Child2* b);

...

void interact(Child2* a, Child1* b)
{
    interact(b, a); // if order does not matter, reuse another function 
}

显然,它不会解决样板代码的问题,但至少可以帮助您 re-think 您的设计并找到比双重分派或转换更好的解决方案。

此外,根据函数内部结构,您可能可以通过使用模板函数而不是重载函数来轻松减少编写(但不是代码大小)。

使用双重分派的@imreal 答案是正确的。但是,可以使用虚拟成员函数而不是映射和函数指针(实际上类似于编译器生成的 vtable)来完成分派。

问题是单个虚函数无法解决问题,因为您确实需要双重分派(即关于两个对象的虚调用,而不仅仅是被调用的对象)。

请参阅以下工作示例:

#include <iostream>

class Child1;
class Child2;

class Parent
{
public:
    virtual void interact_with(Parent* other) = 0;
    virtual void interact_with(Child1* child) {};
    virtual void interact_with(Child2* child) {};
};

class Child1 : public Parent
{
public:
    virtual void interact_with(Parent* other)
    {
        other->interact_with(this);
    }
    virtual void interact_with(Child1* child)
    {
        std::cout << "Child1 - Child1\n";
    }
    virtual void interact_with(Child2* child)
    {
        std::cout << "Child1 - Child2\n";
    }
};

class Child2 : public Parent
{
public:
    virtual void interact_with(Parent* other)
    {
        other->interact_with(this);
    }
    virtual void interact_with(Child1* child)
    {
        std::cout << "Child2 - Child1\n";
    }
    virtual void interact_with(Child2* child)
    {
        std::cout << "Child2 - Child2\n";
    }
};

int main()
{
    Child1 c1;
    Parent* p1 = &c1; // upcast to parent, from p1, we don't know the child type
    Child2 c2;
    Parent* p2 = &c2;

    c1.interact_with(&c2); // single virtual call to Child1 - Child2
    p1->interact_with(&c2); // single virtual call to Child1 - Child2
    p1->interact_with(p2); // double virtual call to Child2 - Child1 (NB: reversed interaction)
}

它输出:

Child1 - Child2
Child1 - Child2
Child2 - Child1

注意最后一个是反的。那是因为要在参数上使用虚函数进行动态分派,我必须将 this 指针与参数交换。如果这些相互作用是对称的,这很好。如果不是,那么我建议围绕最通用的包装器创建一个包装器,再次交换 this 和参数。