C++中的双重调度不起作用

Double dispatch in C++ not working

本文关键字:调度 不起作用 C++      更新时间:2023-10-16

我正在用C++编写一个类似流氓的游戏,并且在双重调度方面遇到了问题。

class MapObject {
virtual void collide(MapObject& that) {};
virtual void collide(Player& that) {};
virtual void collide(Wall& that) {};
virtual void collide(Monster& that) {};
};

然后在派生类中:

void Wall::collide(Player &that) {
that.collide(*this);
}
void Player::collide(Wall &that) {
if (that.get_position() == this->get_position()){
this->move_back();
}
}

然后我尝试使用代码:

vector<vector<vector<shared_ptr<MapObject>>>> &cells = ...

其中创建单元格,如下所示:

objs.push_back(make_shared<Monster>(pnt{x, y}, hp, damage)); //and other derived types
...
cells[pos.y][pos.x].push_back(objs[i]);

当我尝试碰撞玩家和墙壁时:

cells[i][j][z]->collide(*cells[i][j][z+1]);

玩家与基类碰撞,但不与墙壁碰撞。 我做错了什么?

这比仅仅解决你的问题更复杂。 您正在执行手动双重调度,并且存在错误。 我们可以修复您的错误。

但是您遇到的问题不是您的错误,而是您正在手动双重调度的事实。

手动双重派单容易出错。

每次添加新类型时,都必须编写 O(N( 新代码,其中 N 是现有类型的数目。 此代码是基于复制粘贴的,如果您犯了错误,它们会静默地继续错误地发送一些极端情况。

如果您继续执行手动双重调度,则在您或其他任何人修改代码时,您将继续遇到错误。

C++不提供自己的双重调度机制。 但是使用 c++17,我们可以自动编写它


这是一个需要线性工作来管理双重调度的系统,以及每次碰撞的工作。

对于双重调度中的每个类型,您向pMapType添加一个类型。 就是这样,调度的其余部分是为您自动编写的。 然后继承collide_dispatcher<X>的新地图类型X

如果希望两种类型具有冲突代码,请编写一个自由函数do_collide(A&,B&)pMapType变体中更容易的应该是A. 必须先定义此功能,然后才能定义AB才能使调度正常工作。

如果运行a.collide(b)b.collide(a),该代码将运行,其中AB分别是ab的动态类型。

您也可以do_collide成为一种或另一种类型的朋友。

事不宜迟:

struct Player;
struct Wall;
struct Monster;
using pMapType = std::variant<Player*, Wall*, Monster*>;
namespace helper {
template<std::size_t I, class T, class V>
constexpr std::size_t index_in_variant() {
if constexpr (std::is_same<T, std::variant_alternative_t<I, V>>{})
return I;
else
return index_in_variant<I+1, T, V>();
}
}
template<class T, class V>
constexpr std::size_t index_in_variant() {
return helper::index_in_variant<0, T, V>();
}
template<class Lhs, class Rhs>
constexpr bool type_order() {
return index_in_variant<Lhs*, pMapType>() < index_in_variant<Rhs*, pMapType>();
}
template<class Lhs, class Rhs>
void do_collide( Lhs&, Rhs& ) {
std::cout << "Nothing happensn";
}
struct MapObject;
template<class D, class Base=MapObject>
struct collide_dispatcher;
struct MapObject {
virtual void collide( MapObject& ) = 0;
protected:
template<class D, class Base>
friend struct collide_dispatcher;
virtual void collide_from( pMapType ) = 0;
virtual ~MapObject() {}
};
template<class D, class Base>
struct collide_dispatcher:Base {
D* self() { return static_cast<D*>(this); }
virtual void collide( MapObject& o ) final override {
o.collide_from( self() );
}
virtual void collide_from( std::variant<Player*, Wall*, Monster*> o_var ) final override {
std::visit( [&](auto* o){
using O = std::decay_t< decltype(*o) >;
if constexpr( type_order<D,O>() ) {
do_collide( *self(), *o );
} else {
do_collide( *o, *self() );
}
}, o_var );
}
};
void do_collide( Player& lhs, Wall& rhs );
void do_collide( Player& lhs, Monster& rhs );
struct Player : collide_dispatcher<Player> {
friend void do_collide( Player& lhs, Wall& rhs ) {
std::cout << "Player hit a Walln";
}
friend void do_collide( Player& lhs, Monster& rhs ) {
std::cout << "Player fought a Monstern";
}
};
void do_collide( Wall& lhs, Monster& rhs );
struct Wall : collide_dispatcher<Wall> {
friend void do_collide( Wall& lhs, Monster& rhs ) {
std::cout << "Wall blocked a Monstern";
}
};
void do_collide( Monster& lhs, Monster& rhs );
struct Monster : collide_dispatcher<Monster> {
friend void do_collide( Monster& lhs, Monster& rhs ) {
std::cout << "Monster Match!n";
}
};

活生生的例子。

虽然这里的管道很复杂,但这确实意味着您没有手动进行任何双重调度。 您只是在编写端点。 这减少了可能存在极端大小写拼写错误的位置数量。

测试代码:

int main() {
MapObject* pPlayer = new Player();
MapObject* pWall = new Wall();
MapObject* pMonster = new Monster();
std::cout << "Player:n";
pPlayer->collide(*pPlayer);
pPlayer->collide(*pWall);
pPlayer->collide(*pMonster);
std::cout << "Wall:n";
pWall->collide(*pPlayer);
pWall->collide(*pWall);
pWall->collide(*pMonster);
std::cout << "Monster:n";
pMonster->collide(*pPlayer);
pMonster->collide(*pWall);
pMonster->collide(*pMonster);
}

输出为:

Player:
Nothing happens
Player hit a Wall
Player fought a Monster
Wall:
Player hit a Wall
Nothing happens
Wall blocked a Monster
Monster:
Player fought a Monster
Wall blocked a Monster
Monster Match!
您还可以为std::variant<Player*, Wall*, Monster*>

创建一个中央 typedef,并让map_type_index使用该中央 typedef 来确定其顺序,从而将向双重调度系统添加新类型的工作减少到在单个位置添加类型,实现新类型,并向前声明应该执行某些操作的冲突代码。

更重要的是,这种双重调度代码可以使继承友好;从Wall派生的类型可以调度到Wall重载。 如果你想要这个,你必须使collide_dispatcher方法重载非final,允许SpecialWall重载它们。

这是 c++17,但每个主要编译器的当前版本现在都支持它所需要的。 一切都可以在 c++14 甚至 c++11 中完成,但它变得更加冗长,可能需要提升。

虽然定义发生的情况需要线性数量的代码,但编译器将生成二次数量的代码或静态表数据来实现双重调度。 因此,在双重调度表中有 10,000+ 种类型之前要小心。


如果您希望MapObject具体,请从中分离出接口,并从调度程序中删除final,并向pMapType添加MapObject

struct Player;
struct Wall;
struct Monster;
struct MapObject;
using pMapType = std::variant<MapObject*, Player*, Wall*, Monster*>;
namespace helper {
template<std::size_t I, class T, class V>
constexpr std::size_t index_in_variant() {
if constexpr (std::is_same<T, std::variant_alternative_t<I, V>>{})
return I;
else
return index_in_variant<I+1, T, V>();
}
}
template<class T, class V>
constexpr std::size_t index_in_variant() {
return helper::index_in_variant<0, T, V>();
}
template<class Lhs, class Rhs>
constexpr bool type_order() {
return index_in_variant<Lhs*, pMapType>() < index_in_variant<Rhs*, pMapType>();
}
template<class Lhs, class Rhs>
void do_collide( Lhs&, Rhs& ) {
std::cout << "Nothing happensn";
}
struct collide_interface;
template<class D, class Base=collide_interface>
struct collide_dispatcher;
struct collide_interface {
virtual void collide( collide_interface& ) = 0;
protected:
template<class D, class Base>
friend struct collide_dispatcher;
virtual void collide_from( pMapType ) = 0;
virtual ~collide_interface() {}
};
template<class D, class Base>
struct collide_dispatcher:Base {
D* self() { return static_cast<D*>(this); }
virtual void collide( collide_interface& o ) override {
o.collide_from( self() );
}
virtual void collide_from( pMapType o_var ) override {
std::visit( [&](auto* o){
using O = std::decay_t< decltype(*o) >;
if constexpr( type_order<D,O>() ) {
do_collide( *self(), *o );
} else {
do_collide( *o, *self() );
}
}, o_var );
}
};
struct MapObject:collide_dispatcher<MapObject>
{
/* nothing */
};

活生生的例子。

由于您希望PlayerMapObject下降,因此您必须使用collide_dispatcherBase参数:

void do_collide( Player& lhs, Wall& rhs );
void do_collide( Player& lhs, Monster& rhs );
struct Player : collide_dispatcher<Player, MapObject> {
friend void do_collide( Player& lhs, Wall& rhs ) {
std::cout << "Player hit a Walln";
}
friend void do_collide( Player& lhs, Monster& rhs ) {
std::cout << "Player fought a Monstern";
}
};
void do_collide( Wall& lhs, Monster& rhs );
struct Wall : collide_dispatcher<Wall, MapObject> {
friend void do_collide( Wall& lhs, Monster& rhs ) {
std::cout << "Wall blocked a Monstern";
}
};
void do_collide( Monster& lhs, Monster& rhs );
struct Monster : collide_dispatcher<Monster, MapObject> {
friend void do_collide( Monster& lhs, Monster& rhs ) {
std::cout << "Monster Match!n";
}
};

基类应该是:

class MapObject {
public:
virtual ~MapObject () = default;
virtual void collide(MapObject& that) = 0; // Dispatcher
virtual void collide(Player& that) = 0;  // Both types known
virtual void collide(Wall& that) = 0;    // Both types known
virtual void collide(Monster& that) = 0; // Both types known
};

并且您的 Player 类应如下所示:

class Player {
public:
void collide(MapObject& that) override { that.collide(*this); } // dispatch done,
// call true virtual collision code
// Real collision code, both types are known
void collide(Player& that) override  { PlayerPlayerCollision(*this, that);}
void collide(Wall& that) override    { PlayerWallCollision(*this, that);}
void collide(Monster& that) override { MonterPlayerCollision(that, this);}
};

如果你的基类不是抽象的,并且意味着有自己的碰撞, 然后,您必须重命名以区分调度程序和真正的冲突代码:

class MapObject {
public:
virtual ~MapObject () = default;
virtual void collide_dispatcher(MapObject& that) { that.collide(*this); }
// Real collision code, both types are known
virtual void collide(MapObject& that) { MapObjectMapObjectCollision(*this, that);}
virtual void collide(Player& that)    { MapObjectPlayerCollision(*this, that);}
virtual void collide(Wall& that)      { MapObjectWallCollision(*this, that); }
virtual void collide(Monster& that)   { MapObjectMonsterCollision(*this, that); }
};

并且您的 Player 类应如下所示:

class Player {
public:
virtual void collide_dispatcher(MapObject& that) { that.collide(*this); }
// Real collision code, both types are known
void collide(MapObject& that) override { MapObjectPlayerCollision(that, *this);}
void collide(Player& that) override    { PlayerPlayerCollision(*this, that);}
void collide(Wall& that) override      { PlayerWallCollision(*this, that);}
void collide(Monster& that) override   { MonterPlayerCollision(that, this);}
};

看起来像个小错误。

if (that.get_position() == this->get_position())

这绝不是真的。更重要的是,我认为您不需要它,但这不是一个问题。 我认为您需要更改此行

cells[i][j][z]->collide(*cells[i][j][z+1]);

player[i][j][z+1]->collide(*cells[i][j][z+1]);

玩家会撞墙。