【问题标题】:Understanding double dispatch C++了解双重调度 C++
【发布时间】:2021-08-05 09:02:49
【问题描述】:

我试图了解双重调度的工作原理。我创建了一个从抽象类 Creature 派生的怪物和战士可以战斗的示例。 Creature 类有方法“fight”,它在派生类中定义,并且在每个派生类中定义了如果战士与战士或怪物等战斗会发生什么。我编写了以下代码:

#include<iostream>
using namespace std;

class Monster;
class Warrior;

class Creature{
public:
    virtual void fight(Creature&) =0;
};

class Monster: public Creature{
    void fightwho(Warrior& w) {cout<<"Monster versus Warrior"<<endl; }
    void fightwho(Monster& m) {cout<<"Monster versus Monster"<<endl; }
public:
    void fight(Creature& c)  {c.fightwho(*this);}
};

class Warrior: public Creature{
    void fightwho(Warrior& w) {cout<<"Warrior versus Warrior"<<endl; }
    void fightwho(Monster& m) {cout<<"Monster versus Warrior"<<endl; }
public:
    void fight(Creature& c) {c.fightwho(*this);}
};

int main()
{
Warrior w;
Monster m;
w.fight(m);
}

这会导致编译器错误,我预见到:

ex12_10.cpp:在成员函数“virtual void Monster::fight(Creature&)”中:ex12_10.cpp:17:30:错误:“class Creature”没有名为“fightwho”的成员

ex12_10.cpp:在成员函数“virtual void Warrior::fight(Creature&)”中:ex12_10.cpp:24:29:错误:“class Creature”没有名为“fightwho”的成员

但我不知道如何从这里开始......请帮助。

【问题讨论】:

  • 你在关注哪本书/教程?

标签: c++ double-dispatch


【解决方案1】:

嗯,很明显,你真的没有在你的Creature 类中声明fightwho,所以你需要在那里声明它,并将它声明为virtual

双重调度的工作方式是调用(假设Warrior&amp; w = ...,而不是Warrior w):

w.fight(m);

首先虚拟机制将选择Warrior::fight而不是Monster::fight,然后重载机制将选择Monster::fightwho(Warrior&amp; m)而不是Warrior::fightwho(Warrior&amp; m)。请注意,如果您有以下内容会更有意义:

Warrior w;
Monster m;
Creature& c1 = w;
Creature& c2 = m;
c1.fight(c2); // not w.fight(m)

因此,最终将被调用的方法将根据您调用它的对象的类型和作为参数发送的对象的类型进行调度,即双重调度

另外,请注意,这可能不是最好的示例,因为您的类型是同一层次结构的成员。访问者设计模式是一个很好的例子,在不支持它作为一等公民的语言中实现双重分派(即 C++ 和衍生品:Java、C#...)

正如@CrazyCasta 正确指出的那样,当您的类层次结构开始增长时,这种方法变得更加难以维护,并且可能导致方法数量激增,因此请谨慎选择...

【讨论】:

  • 当我将此行添加到类 Creature: virtual void Fightwho(Creature&); 中时,我得到 /tmp/cceFWViM.o: 在函数 Creature::Creature()': ex12_10.cpp:(.text._ZN8CreatureC2Ev[_ZN8CreatureC5Ev]+0xf): undefined reference to vtable for Creature' /tmp/cceFWViM.o: (.rodata._ZTV7Warrior[战士的vtable]+0x18):未定义引用Creature::fightwho(Creature&amp;)' /tmp/cceFWViM.o:(.rodata._ZTV7Monster[vtable for Monster]+0x18): undefined reference to Creature::fightwho(Creature&)' /tmp/cceFWViM.o:(.rodata._ZTI7Warrior[战士的类型信息]+0x10):未定义参考 `typeinfo for Creatur
  • 你需要声明为纯虚拟或者提供body。
  • 如果我将“virtual void Fightwho(Warrior&)=0; virtual void Fightwho(Monster&)=0;”添加到 Creature 就可以了。但是是否可以在不命名怪物和战士的生物中实施?如果我添加“虚空战斗谁(Creature&)=0;”它不起作用
  • 不,这是不可能的,你需要在 Creature 中使用 Fightwho(Warrior&) 和 Fightwho(Monster&),因此这会很快变得非常糟糕。
  • @CrazyCasta 你是对的,当然。我注意到缺少 =0 并完全忽略了这个论点
【解决方案2】:

我对上述答案的贡献是提供经过充分测试的示例,以阐明现实中的双重调度概念。如果您查看以下代码,您会找到我如何自己实现的答案。

#include <iostream>

using namespace std;

class A;
class A1;
class A2;
class B1;
class B2;

class B {
    public:
        // dispatcher function to A
        virtual void collide(const A& a) const = 0;

        // actual collision logic B with types of A
        virtual void collide(const A1& a) const = 0;
        virtual void collide(const A2& a) const = 0;
};

class A {
    public:
        // dispatcher function to B
        virtual void collide(const B& b) const = 0;

        // actual collision logic A with types of B
        virtual void collide(const B1& b) const = 0;
        virtual void collide(const B2& b) const = 0;
};

class A1 : public A {
    public:
        void collide(const B& b) const {
            // dispatch to b
            b.collide(*this);
        }
        void collide(const B1& b) const {
            cout << "collision with B1 and A1" << endl;
        }
        void collide(const B2& b) const {
            cout << "collision with B2 and A1" << endl;
        }
};

class A2 : public A {
    public:
        void collide(const B& b) const {
            // dispatch to a
            b.collide(*this);
        }
        void collide(const B1& b) const {
            cout << "collision with B1 and A2" << endl;
        }
        void collide(const B2& b) const {
            cout << "collision with B2 and A2" << endl;
        }
};

class B1 : public B {
    public:
        void collide(const A& b) const {
            b.collide(*this);
        }
        void collide(const A1& b) const {
            cout << "collision with A1 Bnd B1" << endl;
        }
        void collide(const A2& b) const {
            cout << "collision with A2 Bnd B1" << endl;
        }
};

class B2 : public B {
    public:
        void collide(const A& a) const {
            a.collide(*this);
        }
        void collide(const A1& a) const {
            cout << "collision with A1 Bnd B2" << endl;
        }
        void collide(const A2& a) const {
            cout << "collision with A2 Bnd B2" << endl;
        }
};

int main() {

    A* a = new A1();
    B* b = new B2();

    // first dispatch is done by polymorphism ( a is resolved as a A1 )
    // second dispatch is done in collide function by the function overloading
    // ( in collide function we are sending A1 to collide function of B )
    a->collide(*b);

}

【讨论】:

    【解决方案3】:

    如果您想这样做,您将需要使用 RTTI。你需要检查传入的东西的类型。一般来说,如果你可以避免的话,这不是最好的设计模式。如果要与两个对象交互,通常需要使用另一个对象的标准接口。例如,您可能会说 bio.attack(other_creature) 和 attack 可能会查询另一个生物的防御,并基于此和它自己的统计信息发布对 other_creature 的 hp 更新。

    【讨论】:

    • 双重调度的要点是避免基于RTTI的显式类型选择。
    • 您的方法需要在基类中为您想要与之交互的每种类型的类(很可能是从基类派生的每种类型)提供一个抽象函数。这很快就会变得非常丑陋。
    • 我完全同意 - 这就是为什么我不是特别喜欢访问者模式,尤其是当层次结构开始增长时 - 它很快就会变成一团糟。
    • 使用基本抽象类有什么更好的方法?
    • 我不知道在 C++(和派生语言)中双重分派的更好选择,因为您需要明确提供所有组合。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-12
    • 2018-04-04
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多