【问题标题】:Special interaction between derived objects (i.e. mutiple dispatch)派生对象之间的特殊交互(即多重分派)
【发布时间】:2009-11-23 15:32:22
【问题描述】:

所以,我有一个基类指针列表:

list<Base*> stuff;

然后,在某个时候,其中一个对象将查看所有其他对象。

Base * obj = ...; // A pointer from the 'stuff'-list.
for (list<Base*>::iterator it = stuff.begin(); it != stuff.end(); it++)
{
    if (obj == *it)
        continue;

    // Problem scenario is here
   obj->interact(it);
}

我想要实现的是,根据派生类型obj*it 的不同,它们之间的交互方式会有所不同,即DerivedA 在与DerivedB 交互时会自行销毁,但前提是DerivedB 已设置属性 bool c = true;。所以像:

struct Base 
{
    virtual void interact(Base * b); // is always called
};
struct DerivedA : public Base 
{
    virtual void interact(Base * b){} // is never called
    virtual void interact(DerivedB * b) // is never called
    {
        if (b->c)
            delete this;
    }
};
struct DerivedB : public Base 
{
    bool c = false;
    virtual void interact(Base * b){} // is never called
    virtual void interact(DerivedA * a) // is never called
    {
        c = true;
    }
};
// and many many more Derived classes with many many more specific behaviors.

在编译时,它们都是Base-pointers 并且不能互相调用并期望类型神奇地出现。如果这是一种单向关系,即我知道其中一种是什么类型,我可以使用Visitor pattern。我相信我应该使用某种Mediator pattern,但无法真正弄清楚如何因为调解员也将持有Base-pointers,因此它不会有所作为。

我不知道如何继续……有人吗?


背景:

我正在创建一个游戏,这个问题源于 Room 类,他们跟踪它的内容,即 GameObjects 当前在房间里的内容。

有时,一个物体会移动(例如,玩家)。然后房间将遍历即将移动的地砖上的所有对象(上面的循环),并检查对象是否会相互交互。

例如,如果它是TrollPlayer 会想要伤害它。或者他只是想伤害来自任何另一个“团队”(可以从函数getAlignment()访问,所有@ 987654343@实现)。

【问题讨论】:

  • 你想知道背景故事,因为我可能从错误的方向来处理这个问题?
  • 正确。通常继承用于解耦,当您处理具有特殊情况的对象时,您会遇到这种情况。一般来说,如果我们了解大局,我们可以更好地回答:)
  • 我添加了一些澄清代码以及背景故事,希望它能澄清问题。
  • 在实际系统中,Base::interact()是否被标记为虚拟?
  • 您正在寻找的是所谓的“双重派遣”或“多重派遣”。它在某些语言中原生存在(例如,带有“多方法”的 Common Lisp),但就像几乎所有其他语言一样,可以将其入侵到 C++ 中。

标签: c++ design-patterns inheritance multiple-dispatch


【解决方案1】:

如果可以,请获取“More Effective C++”的副本,并查看关于实现多分派的第 31 项,这基本上就是您在此处寻找的内容。 Meyers 讨论了解决该问题的几种方法及其各种权衡。 (他甚至以游戏为例。)

不过,也许他给出的最佳建议是尝试重新设计您的代码以避免需要此功能。文中还探讨了非成员函数方法,它的额外好处是消除了描述交互的每个函数应该属于哪个对象的问题。

【讨论】:

  • 另一本书建议:Alexandrescu 的《现代 C++ 设计》,第 10 章(“访客”)和第 11 章(“多方法”)
  • 现在我真的不知道如何重新设计我的代码。但是这本书对这类问题有一些非常好的见解,它最终会解决我的问题。
【解决方案2】:

我认为您建议的想法(使用Base::interact)功能几乎完成了。似乎唯一缺少的部分是:

在您的Base 中,您需要拥有子类型的所有interact 重载。考虑对您的 Base 结构进行此扩展:

struct DerivedA;
struct DerivedB;
struct Base 
{
    virtual void interact(Base * b); // *<->Base interaction
    virtual void interact(DerivedA * da); // *<->DerivedA interaction
    virtual void interact(DerivedB * db); // *<->DerivedB interaction
};

在 C++ 中实现双重调度是一件很痛苦的事情:如果添加新的子类型,则必须触及层次结构的基础。

【讨论】:

  • 这只是 Mike B 提议的双重调度的一个例子。
  • +1,因为我可能不得不采用这种方法,除非我创建函数指针映射或类似的东西。
【解决方案3】:

您需要将所有可能的类型组合实现为层次结构之上的虚函数。 这是一个测试所有可能交互的示例:

#include<iostream>
void say(const char *s){std::cout<<s<<std::endl;}
struct DerivedA;
struct DerivedB;
struct Base{
  virtual void interact(Base *b) = 0;

  virtual void interactA(DerivedA *b) = 0;
  virtual void interactB(DerivedB *b) = 0;

};
struct DerivedA : public Base 
{
  virtual void interact(Base *b){
    b->interactA( this ); 
  }
  virtual void interactA(DerivedA *b){
     say("A:A");
  }
  virtual void interactB(DerivedB *b){
     say("A:B");
  }
};
struct DerivedB:public Base{
  virtual void interact(Base *b){
    b->interactB( this ); 
  }
  virtual void interactA(DerivedA *b){
     say("B:A");
  }
  virtual void interactB(DerivedB *b){
     say("B:B");
  }
};

void interact(Base *b1,Base *b2){
  b1->interact( b2 );
}
main(){
  Base *a = new DerivedA;
  Base *b = new DerivedB();

  interact(a,b);
  interact(b,a);
  interact(a,a);
  interact(b,b);
}

【讨论】:

  • 这只是 Mike B 提议的双重调度的一个可能示例。
【解决方案4】:

第一:你为什么用struct而不是class

第二:如果你使用class 而不是struct,你可以(必须)做这样的事情:

class Base 
{
    virtual void interact(Base * b); // see the VIRTUAL word (explained down)
}; 
class DerivedA : public Base 
{
    virtual void interact(DerivedB * b)
    {
        if (b->c)
            delete this;
    }
};
class DerivedB : public Base 
{
    bool c = false;
    virtual void interact(DerivedA * a)
    {
        c = true;
    }
};

使用virtual 关键字是你需要的(我猜)。如果您将方法定义为virtual,您是在告诉“嘿!这可能已在某个地方被覆盖”所以.. 当您编写此代码时:

DerivedA* a = new DerivedA();
DerivedB* b = new DerivedB();
a.interact(b); // here instead of calling Base::interact(Base*) call the override method in DerivedA class (because is virtual)

编辑:

忘记那个答案了..(没看到virtual的评论)

编辑 2:

请参阅catwalkFrerich Raabe 的答案。

【讨论】:

  • 我只在示例代码中使用 struct,因为如果我使用类,我必须写 public: 到处都是。在实际应用中,我使用类。
【解决方案5】:

您的 interact() 函数没有相同的签名:在派生类中它们也应该是

virtual void interact(Base * b);

virtual 当然是可选的,但为了清楚起见,我会把它放在那里。

要确定 DerivedA::interact() 是否应该对它的参数做一些事情,你可以在你的基类中实现另一个虚函数:

virtual canDoX(Base * b);
virtual canDoY(Base * b);

那么在派生的实现中它可能看起来像这样:

// DerivedA
void interact(Base * b)
{
    if (b->canDoX() && b->c)
        delete this;
}

// DerivedB
void interact(Base * b)
{
    if(b->canDoY())
        c = true;
}

更新: 既然您喜欢 Frerich Raabe 的回答,让我解释一下为什么我认为我的方法更好一点。
按照他的建议,必须为基中的每个派生类和所有其他可以与某个类交互的派生类创建一个interact() 方法。
使用我的解决方案,必须为某些属性添加方法,这些方法也可以组合。 如果您有一个 Troll,它将在其 canBeKilled() 方法中返回 true。一个苹果 canBeEaten() 和一个美味的野生动物 canBeKilled() 然后是 canBeEaten()
如果可以组合属性,则必须添加更少的功能。

此外:如果巨魔喝了一些长生不老药使其在一段时间内无懈可击,它会返回canBeKilled() == false,仅此而已。您不必在其他交互类中检查isInvulnerable() 标志。

【讨论】:

  • 我把代码澄清了一点,我从头开始写作为例子。
  • 这确实是一个解决方案,但它并不漂亮,并且类将非常紧密地耦合。
【解决方案6】:

我认为您的问题在 Scott Meyer 的 Effective C++ 如下:

规则:不要尝试使用基类指针访问派生类对象的数组 -->结果将是未定义的。

我给你举个例子:

结构基础{

virtual void print() { //in base }

虚拟的~Base() {} //

}

结构派生:公共基础{

virtual void print(){ //在派生中 }

}

void foo(Base *pBase,int N) {

for(int i=0; i<N ; i++)
   pBase[i].print(); // result will be undefined......

}

int main() {

 Derived d[5];
 foo(&d,5); 

}

这种行为的原因是编译器在 sizeof(Base) 字节的跳转处找到下一个元素....

我想你明白我的意思......

【讨论】:

  • 那么你的意思是房间跟踪一堆游戏对象的整个概念是一个不好的方法?你有更好的建议吗?
  • @unknown:为什么要未定义?这正是虚函数的用途。此外,您的示例无效,您缺少foo() 的参数。
  • 不,如果您将对象存储为指针列表(如您在示例中所示),那么这不适用。
  • 它是未定义的(在这种情况下) sizeof(Base) != sizeof(Derived)。假设 base 是 2 个字节,而 derived 是 4 个字节,以这种方式迭代数组 pBase[i] 将计算到实际对象的错误偏移量。
  • Brr,刚刚注意到情况更糟。 [] 运算符在该示例中返回一个实例,而不是指针 - 那么 -&gt; 在那里做什么? ;)
猜你喜欢
  • 1970-01-01
  • 2020-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多