C++ - 如何使用基指针覆盖子类方法

Posted

技术标签:

【中文标题】C++ - 如何使用基指针覆盖子类方法【英文标题】:C++ - How to override child class methods using base pointers 【发布时间】:2017-03-11 15:35:06 【问题描述】:

所以。

我正在为学校做一个小型游戏项目,我的对象有碰撞和东西,我的想法是检查一个对象是否与另一个对象发生碰撞,然后让每种类型的特定逻辑都有一堆重载功能取决于发送的对象。例如,如果对象是玩家控制的对象,敌人会伤害它,但如果是与敌人碰撞的通电,一切都会好起来的。

所以,我希望我可以做类似的事情(显然一切都继承自 Obj):

std::vector<Obj*> list;
list.push_back(new Powerup());
list.push_back(new Enemy());
list.push_back(new Player());

for (auto i: list) 
    for (auto j: list) 
        if (collision(i,j)) 
            i->doStuff(*j);
        
    

但是我很难找到发送正确类型的方法。我做了一个测试程序来演示这个问题:

#include <iostream>

class A 
    public:
        virtual void doStuff(A& T)  std::cout << "A->A!" << std::endl; 
;

class B : public A 
    public:
        virtual void doStuff(A& T)  std::cout << "B->A!" << std::endl; 
;

class C : public A 
    public:
        virtual void doStuff(A& T)  std::cout << "C->A!" << std::endl; 
        virtual void doStuff(B& T)  std::cout << "C->B!" << std::endl; 
;

int main() 
    A* base;
    A  a;
    B  b;
    C  c;

    c.doStuff(a);
    c.doStuff(b);

    base = &a;
    c.doStuff(*base);

    base = &b;
    c.doStuff(*base);

    return 0;

运行它我得到了这个:

C->A!
C->B!
C->A!
C->A!

当我期待时:

C->A!
C->B!
C->A!
C->B!

知道如何进行这项工作吗?

【问题讨论】:

【参考方案1】:

您在这里尝试的称为 双重调度 - 使函数相对于两个参数成为虚拟函数。与大多数具有虚函数的编程语言一样,C++ 本身并不支持这一点。


考虑c.doStuff(*base); 电话。它实际上有两个参数:c(在函数内部以*this结尾)和*base

现在的问题是doStuff 仅相对于第一个参数是虚拟的。这就是为什么即使c 具有静态基类类型,调用也会以CdoStuff 函数之一结束。然而,第二个参数不是以多态方式处理的。 base 的静态类型是 A*,即使它当前指向子类对象,所以 *base 产生 A,并且与 A&amp; 重载匹配。


虽然 C++ 确实不支持双重分派,但有一些方法可以模拟它。访问者设计模式经常被引用为一种方法。例如,请参阅Difference betwen Visitor pattern & Double Dispatch。


P.S.:将覆盖与重载混合并不是一个好主意,因为作为代码的人类读者,找出将调用哪个函数会变得非常混乱。

【讨论】:

【参考方案2】:

问题是你最后一次调用doStuff你传递了一个A&amp;所以它会调用C::doStuff(A&amp;)。如果您希望它调用C::doStuff(B&amp;),您需要将base 转换为B&amp;。这可以通过static_castdynamic_cast 完成。

你应该看看 Double Dispatch:Understanding double dispatch C++

【讨论】:

好的,所以我认为我必须这样做: void doStuff(A& T) A *a = dynamic_cast(T); B *b = dynamic_cast(T); if (a) std::cout A!" B!" 查看双重调度。如果您不想为每个派生类型编写强制转换,这就是您想要的。仅当您不知道 B 派生 A 时,动态转换才有用。如果 B 不派生 A,它将返回 NULL/nullptr。这是一个缓慢的运行时操作。如果您知道 B 派生 A,请使用 static_cast,否则您将获得 UB。【参考方案3】:

问题是 C++ 使用单调度,而您想要的是双调度。一种说法是,单次调度使用编译时可用的信息(您将 A 类型的指针传递给函数)而不是运行时已知的信息(A 类型的指针恰好指向 B 类型的对象因此,在决定要调用的函数的重载时,您想调用 B) 的重载。

一些很好的资源:@​​987654321@ 和 https://en.wikipedia.org/wiki/Double_dispatch

单一调度编程语言通常解决这个问题的一种方式,无需转换是使用“访问者模式”。

class DoStuffClass //visitor class
public:
    void doStuff(B &b);//traditionally visit(B &b)
    void doStuff(C &c);
    void doStuff(A &a);
;

class A //visited class
public:
    void interactWith(DoStuffClass &dsc)
        //here's the good part: 
        //Now it is known at compile time(statically) what the type of *this is,
        //so dispatching to the desired function - doStuff(A&) - can happen
        doc.doStuff(*this);
    ;


class B: public A //visited class
public:
    void interactWith(DoStuffClass &dsc)
        //same as for A
        doc.doStuff(*this);
    ;

注意:传统上Visitor类的成员类被称为visit(SomeVisitedClass &),被访问的类成员函数被称为receiver(Visitor &)。但是你可以随心所欲地称呼它。

那么你可以这样做:

DoStuffClass dsf;
A a;
B b;
A *aptr;
aptr = &b;
a.interactWith(dsf); //will call the do stuff function that handles A
b.interactWith(dsf); //will call the do stuff function that handles B
aptr->interactWith(dsf);//still calls the do stuff function that handles B

现在,当您使用它时,您还可以看看中介者模式。它旨在减轻需要相互了解才能工作的类之间的依赖关系(例如在您的情况下),并使您不必让每个类都了解所有其他类:

https://en.wikipedia.org/wiki/Mediator_pattern

【讨论】:

呃,在提交之前应该刷新页面:P Christian 已经提到了我所说的部分内容。不过,您可能会发现阅读第一个参考文献和最后一个参考文献(关于 Mediator 模式)很有趣。 感谢您的帮助,我设法使用此处的 3 个答案解决了它。 :) 很高兴听到队友。如果您发现此答案(和/或 Gambit 的)有用,Stack Overflow 会在答案旁边提供向上箭头以将其标记为有用。随意点击。 :)

以上是关于C++ - 如何使用基指针覆盖子类方法的主要内容,如果未能解决你的问题,请参考以下文章

c++ 虚函数 如何调父类 而非子类

C++获取基类指针所指子类对象的类名

c++中如何获取函数名,和获取到一个基类指针后,如何判断它里面保存的是那个子类类型对象?

C++子类如何调父类的虚函数

c ++使基类在子类中使用覆盖的方法

深入理解C++ 虚函数表