C++ 中的双重调度不起作用
Double dispatch in C++ not working
我正在用 C++ 编写一个类似 rogue 的游戏,但遇到双重调度问题。
class MapObject {
virtual void collide(MapObject& that) {};
virtual void collide(Player& that) {};
virtual void collide(Wall& that) {};
virtual void collide(Monster& that) {};
};
然后派生 classes:
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]);
玩家与底座发生碰撞 class,但没有与墙壁发生碰撞。
我做错了什么?
看起来是个小错误。
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]);
玩家会撞到墙上。
你的基础 class 应该是:
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
};
你的播放器 class 应该是这样的:
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 不是抽象的,意味着有自己的碰撞,
那么你必须重命名以区分调度程序和真正的碰撞代码:
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); }
};
你的播放器 class 应该是这样的:
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);}
};
这比解决您的问题要复杂得多。您正在进行手动双重调度,并且存在错误。我们可以修复您的错误。
但是你的问题不是你的错误,而是你在进行手动双重调度。
手动双重调度容易出错。
每次添加新类型,都需要编写 O(N) 新代码,其中 N 是现有类型的数量。这段代码是基于复制粘贴的,如果你犯了错误,他们会默默地继续错误发送一些极端情况。
如果您继续进行手动双重分派,那么每当您或其他任何人修改代码时,您都会继续遇到错误。
C++ 不提供自己的双重分派机制。但是使用 c++17 我们可以自动写入它
这是一个需要线性工作来管理双重调度的系统,加上每次碰撞的工作。
对于双重分派中的每种类型,您都将一种类型添加到 pMapType
。就是这样,调度的其余部分是自动为您编写的。然后从 collide_dispatcher<X>
.
继承新的地图类型 X
如果你想让两个类型有冲突代码,写一个自由函数 do_collide(A&,B&)
。 pMapType
变体中更简单的应该是 A
。此函数必须在定义 A
和 B
之前定义,以便调度工作。
如果 a.collide(b)
或 b.collide(a)
是 运行,则该代码得到 运行,其中 A
和 B
是动态类型a
和 b
分别。
您也可以让 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 happens\n";
}
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 Wall\n";
}
friend void do_collide( Player& lhs, Monster& rhs ) {
std::cout << "Player fought a Monster\n";
}
};
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 Monster\n";
}
};
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 中完成,但它变得更加冗长并且可能需要 boost .
虽然需要线性数量的代码来定义发生的事情,但编译器将生成二次数量的代码或静态 table 数据来实现双重分派。所以在你的双分派中有 10,000 多种类型之前要小心 table.
如果你希望MapObject
是具体的,从中分离出接口并从调度程序中删除final
并将MapObject
添加到pMapType
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 happens\n";
}
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 */
};
因为您希望 Player
从 MapObject
继承,您必须使用 collide_dispatcher
的 Base
参数:
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 Wall\n";
}
friend void do_collide( Player& lhs, Monster& rhs ) {
std::cout << "Player fought a Monster\n";
}
};
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 Monster\n";
}
};
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";
}
};
我正在用 C++ 编写一个类似 rogue 的游戏,但遇到双重调度问题。
class MapObject {
virtual void collide(MapObject& that) {};
virtual void collide(Player& that) {};
virtual void collide(Wall& that) {};
virtual void collide(Monster& that) {};
};
然后派生 classes:
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]);
玩家与底座发生碰撞 class,但没有与墙壁发生碰撞。 我做错了什么?
看起来是个小错误。
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]);
玩家会撞到墙上。
你的基础 class 应该是:
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
};
你的播放器 class 应该是这样的:
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 不是抽象的,意味着有自己的碰撞, 那么你必须重命名以区分调度程序和真正的碰撞代码:
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); }
};
你的播放器 class 应该是这样的:
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);}
};
这比解决您的问题要复杂得多。您正在进行手动双重调度,并且存在错误。我们可以修复您的错误。
但是你的问题不是你的错误,而是你在进行手动双重调度。
手动双重调度容易出错。
每次添加新类型,都需要编写 O(N) 新代码,其中 N 是现有类型的数量。这段代码是基于复制粘贴的,如果你犯了错误,他们会默默地继续错误发送一些极端情况。
如果您继续进行手动双重分派,那么每当您或其他任何人修改代码时,您都会继续遇到错误。
C++ 不提供自己的双重分派机制。但是使用 c++17 我们可以自动写入它
这是一个需要线性工作来管理双重调度的系统,加上每次碰撞的工作。
对于双重分派中的每种类型,您都将一种类型添加到 pMapType
。就是这样,调度的其余部分是自动为您编写的。然后从 collide_dispatcher<X>
.
X
如果你想让两个类型有冲突代码,写一个自由函数 do_collide(A&,B&)
。 pMapType
变体中更简单的应该是 A
。此函数必须在定义 A
和 B
之前定义,以便调度工作。
如果 a.collide(b)
或 b.collide(a)
是 运行,则该代码得到 运行,其中 A
和 B
是动态类型a
和 b
分别。
您也可以让 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 happens\n";
}
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 Wall\n";
}
friend void do_collide( Player& lhs, Monster& rhs ) {
std::cout << "Player fought a Monster\n";
}
};
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 Monster\n";
}
};
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 中完成,但它变得更加冗长并且可能需要 boost .
虽然需要线性数量的代码来定义发生的事情,但编译器将生成二次数量的代码或静态 table 数据来实现双重分派。所以在你的双分派中有 10,000 多种类型之前要小心 table.
如果你希望MapObject
是具体的,从中分离出接口并从调度程序中删除final
并将MapObject
添加到pMapType
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 happens\n";
}
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 */
};
因为您希望 Player
从 MapObject
继承,您必须使用 collide_dispatcher
的 Base
参数:
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 Wall\n";
}
friend void do_collide( Player& lhs, Monster& rhs ) {
std::cout << "Player fought a Monster\n";
}
};
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 Monster\n";
}
};
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";
}
};