Двойная отправка в С++ не работает

Я пишу мошенническую игру на 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]);

Игрок сталкивается с базовым классом, но не со стеной. Что я делаю неправильно?


person user7369463    schedule 24.05.2018    source источник
comment
Что я делаю неправильно? -> Фундаментальная проблема заключается в моем первоначальном предположении, что бизнес-правила системы должны быть выражены путем написания кода внутри методов, связанных с классами в бизнес-домене - ericlippert.com/2015/05/11/wizards-and-warriors-part-five   -  person Caleth    schedule 24.05.2018
comment
Компилятор не может автоматически конвертировать MapObject в Wall. Таким образом, он вызовет функцию Player::collide(MapObject&). Если вы не переопределили его, он попадает в MapObject::collide(MapObject&).   -  person Hans Passant    schedule 24.05.2018
comment
Приведите минимально воспроизводимый пример.   -  person JHBonarius    schedule 24.05.2018


Ответы (3)


Это сложнее, чем просто решить вашу проблему. Вы выполняете ручную двойную отправку, и у вас есть ошибки. Мы можем исправить ваши ошибки.

Но проблема не в ошибках, а в том, что вы выполняете двойную отправку вручную.

Ручная двойная отправка подвержена ошибкам.

Каждый раз, когда вы добавляете новый тип, вам приходится писать O(N) нового кода, где N — количество существующих типов. Этот код основан на копировании и вставке, и если вы делаете ошибки, они молча продолжают неправильно отправлять некоторые угловые случаи.

Если вы продолжите выполнять двойную отправку вручную, у вас будут по-прежнему возникать ошибки всякий раз, когда вы или кто-либо еще изменяет код.

C++ не предоставляет собственного механизма двойной диспетчеризации. Но с c++17 мы можем автоматизировать его написание


Вот система, которая требует линейной работы для управления двойной отправкой плюс работа для каждого столкновения.

Для каждого типа в двойной отправке вы добавляете тип к pMapType. Все, остальная часть рассылки автоматически пишется за вас. Затем наследуйте новый тип карты X от collide_dispatcher<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!

Вы также можете создать центральный typedef для std::variant<Player*, Wall*, Monster*>, а map_type_index использовать этот центральный typedef для определения его порядка, сократив работу по добавлению нового типа в систему двойной диспетчеризации до добавления типа в одном месте, реализации нового типа и переадресации. объявление кода коллизии, который должен что-то делать.

Более того, этот код двойной отправки можно сделать удобным для наследования; тип, производный от Wall, может отправлять в Wall перегрузки. Если вы хотите этого, вы должны сделать перегрузку метода collide_dispatcher не-final, позволяя SpecialWall перезагружать их.

Это c++17, но текущие версии всех основных компиляторов теперь поддерживают то, что им нужно. Все можно сделать в c++ 14 или даже c ++11, но он становится намного более подробным и может потребовать повысить.

В то время как для определения того, что происходит, требуется линейный объем кода, компилятор сгенерирует квадратичный объем кода или статические табличные данные для реализации двойной диспетчеризации. Так что позаботьтесь о том, чтобы в вашей двойной диспетчерской таблице было более 10 000 типов.


Если вы хотите, чтобы 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, вы должны использовать аргумент Base для collide_dispatcher:

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";
  }
};
person Yakk - Adam Nevraumont    schedule 24.05.2018
comment
Для чего нужен дополнительный параметр типа Base в collide_dispatcher? Почему бы просто collide_dispatcher не наследовать напрямую MapObject? - person Caleth; 24.05.2018
comment
@Caleth Обычно так бывает; MapObject — это значение по умолчанию для Base, если вы его не передадите. Но предположим, что у вас есть 5 разных мультидиспетчеров. Это позволяет вам линейно составлять collide_dispatcher-подобные типы. Или, если вы удалите final, он разрешает SpecialWall, производный от Wall, который переопределяет эти методы. - person Yakk - Adam Nevraumont; 24.05.2018
comment
О, так у тебя есть SpecialWall : public collide_dispatcher<SpecialWall, Wall>? - person Caleth; 24.05.2018
comment
@Калет Конечно. Или Wall : public collide_dispatcher< Wall, damage_dispatcher< Wall, DamagableMapObject > >, или даже Wall : mixins< Wall, DamageableMapObject, collide_dispatcher, damage_dispatcher >, и он сшивает воедино (линейную) иерархию наследования, не требуя виртуального вмешательства. По сути, class Base=DefaultBase включает целую кучу несколько полезных возможностей расширения и имеет низкую стоимость, когда не используется, поэтому я использую его при написании миксинов. - person Yakk - Adam Nevraumont; 24.05.2018
comment
map_type_index( Player* ) можно было бы даже реализовать а-ля using VariantObject = std::variant<Player*, Wall*, Monster*>; variant_index<VariantObject, Player*>::value. поэтому следует изменить только вариант typedef (с дополнительной функцией столкновения). - person Jarod42; 24.05.2018
comment
@Jarod42 std, кажется, не имеет variant_index; Я должен был написать это. Разве я пропустил API для него? - person Yakk - Adam Nevraumont; 24.05.2018
comment
Я не добавлял для него std::, так как это действительно должно быть реализовано самим :/ (также не вижу эквивалента для std::tuple :-(). - person Jarod42; 24.05.2018
comment
@Jarod42 Написал это. Это делает его более аккуратным. - person Yakk - Adam Nevraumont; 24.05.2018
comment
Почему f((T*)nullptr) вместо f<T>()? - person Jarod42; 24.05.2018
comment
@ Jarod42 Сейчас index_in_variant<T, Var>(), что достаточно близко. Я использовал (T*)nullptr, потому что собирался позволить людям реализовать эту функцию, используя перегрузку распределенным способом; мы могли бы даже сделать это (время компиляции) double таким образом и позволить вам поместить тип между другими существующими типами и т. д. Но если он выводит свой порядок из центрального варианта, это лучше. - person Yakk - Adam Nevraumont; 24.05.2018
comment
@Caleth Итак, вы увидите, что когда я сделал возможным создание конкретных MapObject, в итоге я использовал параметр Base. - person Yakk - Adam Nevraumont; 24.05.2018

Ваш базовый класс должен быть:

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);}
};
person Jarod42    schedule 24.05.2018
comment
Методы моего базового класса не являются абстрактными, потому что в своем коде я создаю экземпляры базового класса. - person user7369463; 24.05.2018
comment
Однако это правильный подход, и он должен быть совместим с неабстрактными реализациями в базе. То, что у вас есть в исходном сообщении, по существу не является двойной отправкой. У вас только разовая рассылка. Для двойной отправки подкласс должен выполнить обратный вызов вызывающего объекта, используя this, чтобы предоставить правильный тип, а не полиморфный супертип, содержащийся в контейнере. - person Sean Burton; 24.05.2018

Похоже на небольшую ошибку.

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]);

И игрок ударится о стену.

person Marek Adam Niemiec    schedule 24.05.2018
comment
if (that.get_position() == this-›get_position()) не всегда ложно (я написал компаратор для структур). Почему вы хотите, чтобы я изменил z на z+1? Игрок находится в [i][j][0], а стена в [i][j][1] (или наоборот, не имеет значения) - person user7369463; 24.05.2018