Использовать случай dynamic_cast

Во многих местах вы можете прочитать, что dynamic_cast означает "плохой дизайн". Но я не могу найти статью с соответствующим использованием (показывая хороший дизайн, а не только "как использовать").

Я пишу настольную игру с доской и много разных типов карт, описанных со многими атрибутами (некоторые карты можно положить на доску). Поэтому я решил разбить его на следующие классы/интерфейсы:

class Card {};
class BoardCard : public Card {};
class ActionCard : public Card {};
// Other types of cards - but two are enough
class Deck {
    Card* draw_card();
};
class Player {
    void add_card(Card* card);
    Card const* get_card();
};
class Board {
    void put_card(BoardCard const*);
};

Некоторые ребята предположили, что я должен использовать только один класс, описывающий карту. Но я бы назвал много взаимоисключающих атрибутов. А в случае класса Board ' put_card(BoardCard const&) - это часть интерфейса, в которую я не могу поместить какую-либо карточку на борт. Если бы у меня был только один тип карточек, я должен был бы проверить его внутри метода.

Я вижу поток следующим образом:

  • общая карта находится в колоде (это не важно, каков ее тип)
  • общая карта берется из колоды и передается игроку (так же, как указано выше)
  • если игрок выбрал BoardCard, то его можно положить на доску

Поэтому я использую dynamic_cast перед тем, как положить карту на доску. Я думаю, что использование какого-либо виртуального метода в этом случае не может быть и речи (кроме того, я бы не имел смысла добавлять какие-либо действия в отношении платы на каждую карту).

Поэтому мой вопрос: что я плохо разработал? Как я могу избежать dynamic_cast? Использование некоторого атрибута типа, и if бы это было лучшее решение...?

PS Любой источник, рассматривающий использование dynamic_cast в контексте дизайна, более чем оценен.

Ответы

Ответ 1

Да, dynamic_cast это код запах, но так добавление функций, которые пытаются сделать его похожим на вас есть хороший полиморфный интерфейс, но на самом деле равны dynamic_cast т.е. таких вещей, как can_put_on_board. Я бы сказал, что can_put_on_board хуже - вы дублируете код, иначе реализованный dynamic_cast и загромождающий интерфейс.

Как и во всех запахах кода, они должны вас насторожить, и они не обязательно означают, что ваш код плохой. Все зависит от того, чего вы пытаетесь достичь.

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

Предполагая последнее, рассмотрим некоторые альтернативы.

Вы можете наложить бремя на карточку, а не на внешний код. Например, добавьте функцию play(Context& c) к карте (Context - это средство доступа к доске и все, что может быть необходимо). Плата должна знать, что она может быть применена только к доске, и бросок не понадобится.

Однако я бы отказался от использования наследования. Одна из его многочисленных проблем заключается в том, как она вводит категоризацию всех карточек. Позволь мне привести пример:

  • вы приведете BoardCard и ActionCard положить все карты в этих двух ведер;
  • затем вы решите, что вы хотите иметь карту, которая может быть использована двумя способами, либо в качестве Action или Board карты;
  • скажем, вы решили проблему (посредством множественного наследования, типа BoardActionCard или любым другим способом);
  • вы затем решили, что хотите иметь цвета карты (как в MtG) - как вы это делаете? Вы создаете RedBoardCard, BlueBoardCard, RedActionCard т.д.?

Другие примеры того, почему следует избегать наследования и как добиться полиморфизма во время выполнения, в противном случае вы можете посмотреть, как Шон Родитель отлично говорит "Наследование - это базовый класс зла". Многообещающая библиотека, которая реализует этот вид полиморфизма, - это dyno, но я еще не пробовал это.

Возможное решение может быть:

class Card final {
public:
    template <class T>
    Card(T model) :
        model_(std::make_shared<Model<T>>(std::move(model)))
    {}

    void play(Context& c) const {
        model_->play(c);
    }

    // ... any other functions that can be performed on a card

private:

    class Context {
    public:
        virtual ~Context() = default;
        virtual void play(Context& c) const = 0;
    };

    template <class T>
    class Model : public Context {
    public:
        void play(Context& c) const override {
            play(model_, c);

            // or

            model_.play(c);

            // depending on what contract you want to have with implementers
        }
    private:
        T model_;
    };

    std::shared_ptr<const Context> model_;

};

Затем вы можете создать классы для каждого типа карты:

class Goblin final {
    void play(Context& c) const {
        // apply effects of card, e.g. take c.board() and put the card there
    }
};

Или реализовать поведение для разных категорий, например,

template <class T>
void play(const T& card, Context& c);

шаблон, а затем используйте enable_if для обработки его для разных категорий:

template <class T, class = std::enable_if<IsBoardCard_v<T>>
void play(const T& card, Context& c) {
    c.board().add(Card(card));
}

где:

template <class T>
struct IsBoardCard {
    static constexpr auto value = T::IS_BOARD_CARD;
};

template <class T>
using IsBoardCard_v = IsBoardCard<T>::value;

затем определите своего Goblin как:

class Goblin final {
public:
    static constexpr auto IS_BOARD_CARD = true;
    static constexpr auto COLOR = Color::RED;
    static constexpr auto SUPERMAGIC = true;
};

что позволит вам классифицировать свои карты во многих измерениях, оставляя возможность полностью специализировать поведение, реализуя другую функцию play.

В примере кода используется std :: shared_ptr для хранения модели, но вы можете определенно сделать что-то умнее здесь. Мне нравится использовать хранилище статического размера и разрешать использовать только Ts определенного максимального размера и выравнивания. В качестве альтернативы вы можете использовать std :: unique_ptr (который может отключить копирование) или вариант, использующий небольшую оптимизацию.

Ответ 2

Вы можете применить принципы Microsoft COM и предоставить ряд интерфейсов, каждый из которых описывает набор связанных действий. В COM вы определяете, доступен ли какой-либо конкретный интерфейс, вызывая QueryInterface, но в современных C++ dynamic_cast работает аналогично и более эффективен.

class Card {
    virtual void ~Card() {} // must have at least one virtual method for dynamic_cast
};
struct IBoardCard {
    virtual void put_card(Board* board);
};
class BoardCard : public Card, public IBoardCard {};
class ActionCard : public Card {};
// Other types of cards - but two are enough
class Deck {
    Card* draw_card();
};
class Player {
    void add_card(Card* card);
    Card const* get_card();
};
class Board {
    void put_card(Card const* card) {
        const IBoardCard *p = dynamic_cast<const IBoardCard*>(card);
        if (p != null) p->put_card(this);
};

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

Ответ 3

Почему бы не использовать dynamic_cast

dynamic_cast обычно не нравится, потому что его можно легко злоупотреблять, чтобы полностью нарушить используемые абстракции. И нецелесообразно зависеть от конкретных реализаций. Конечно, это может понадобиться, но на самом деле редко, поэтому почти все принимают эмпирическое правило - вероятно, вы не должны его использовать. Это запах кода, который может означать, что вы должны переосмыслить свои абстракции, потому что они могут быть не теми, которые необходимы в вашем домене. Возможно, в вашей игре у Board не должно быть метода put_card - возможно, вместо этого карта должна иметь метод play(const PlaySpace *) где Board реализует PlaySpace или что-то в этом роде. Даже CppCoreGuidelines препятствуют использованию dynamic_cast в большинстве случаев.

При использовании

Как правило, у немногих людей такие проблемы возникают, но я наткнулся на это несколько раз. Проблема называется Double (или Multiple) Dispatch. Вот довольно старая, но вполне уместная статья о двойной отправке (см. Доисторический auto_ptr): http://www.drdobbs.com/double-dispatch-revisited/184405527

Кроме того, Скотт Мейерс в одной из своих книг написал что-то о построении двойной диспетчерской матрицы с dynamic_cast. Но, в целом, эти dynamic_cast "скрыты" внутри этой матрицы - пользователи не знают, какая магия происходит внутри.

Примечательно - многократная отправка также считается запахом кода :-).

Разумная альтернатива

Проверьте шаблон посетителя. Его можно использовать как замену для dynamic_cast но это также своего рода запах кода.

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

Ответ 4

Мне кажется, что два типа карт совершенно разные. То, что может делать карточная карта и карта действий, являются взаимоисключающими, и обычная вещь заключается в том, что их можно вытащить из колоды. Более того, это не то, что делает карта, это действие игрока/колоды.

Если это так, вопрос должен спросить, должны ли они действительно сходить с обычного типа, на Card. Альтернативный дизайн будет иметь тегированный союз: пусть Card вместо этого будет std::variant<BoardCard, ActionCard...> и содержит экземпляр соответствующего типа. При принятии решения о том, что делать с картой, вы используете switch index() а затем std::get<> только соответствующий тип. Таким образом, вам не нужен какой-либо оператор *_cast, и получите полную свободу в отношении того, какие методы (ни один из которых не имеет смысла для других типов), поддерживаемых каждым типом карты.

Если это только почти верно, но не для всех типов, вы можете слегка различать: группируйте только те типы карт, которые могут разумно быть переклассифицированы, и поместите набор этих общих типов в variant.

Ответ 5

Я всегда находил использование литого запаха кода, и, по моему опыту, 90% случаев были сделаны из-за плохого дизайна. Я видел использование dynamic_cast в некотором критическом для времени приложении, где он обеспечивал большее улучшение производительности, чем наследование с нескольких интерфейсов или извлечение нумерации какого-либо объекта из объекта (типа типа). Таким образом, код пахло, но использование динамического броска стоило того в этом случае.

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

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

Я бы выбрал композицию вместо наследования. Это обеспечит вам равноценность использования карты как "фабрики":

  • он может порождать больше модификаторов игры - что-то, что нужно применить к доске, а другое - конкретному противнику
  • карту можно использовать повторно - карта может оставаться в руках игрока, и эффект от игры от нее отделен (между картами и эффектами нет 1-1)
  • сама карта может сидеть сложа руки на палубе, а последствия того, что она делает, все еще живы на доске.
  • карта может иметь представление (методы рисования) и реагировать на касание таким образом, где вместо этого BoardElement может быть равномерно миниатюрным с анимацией

Подробнее см. [ Https://en.wikipedia.org/wiki/Composition_over_inheritance ]. Я бы хотел процитировать: композиция также обеспечивает более стабильный бизнес-домен в долгосрочной перспективе, поскольку он менее подвержен причудам членов семьи. Другими словами, лучше составить то, что может сделать объект (HAS-A), чем расширять то, что оно (IS-A). [1]

BoardCard/Element может быть примерно таким:

//the card placed on the board.
class BoardElement {
public:
  BoardElement() {}
  virtual ~BoardElement() {};

  //up to you if you want to add a read() methods to read data from the card description (XML / JSON / binary data)
  // but that should not be part of the interface. Talking about a potential "Wizard", it probably more related to the WizardCard - WizardElement relation/implementation

  //some helpful methods:
  // to be called by the board when placed
  virtual void OnBoard() {}
  virtual void Frame(const float time) { /*do something time based*/ }
  virtual void Draw() {}
  // to be called by the board when removed
  virtual void RemovedFromBoard() {}
};

Карта может представлять что-то, что будет использоваться в колоде или в руках пользователя, я добавлю такой интерфейс

class Card {
public:
  Card() {}
  virtual ~Card() {}

  //that will be invoked by the user in order to provide something to the Board, or NULL if nothing should be added.
  virtual std::shared_ptr<BoardElement*> getBoardElement() { return nullptr; }

  virtual void Frame(const float time) { /*do something time based*/ }
  virtual void Draw() {}

  //usefull to handle resources or internal states
  virtual void OnUserHands() {}
  virtual void Dropped() {}
};

Я хотел бы добавить, что этот шаблон допускает множество трюков внутри getBoardElement(), начиная с работы в качестве фабрики (поэтому что-то должно быть создано с его собственной продолжительностью жизни), возвращая член данных Card такой как std:shared_ptr<BoardElement> wizard3D; (как пример), создайте привязку между Card и BoardElement как:

class WizardBoardElement : public BoardElement {
public:
  WizardBoardElement(const Card* owner);

  // other members omitted ...
};

Связывание может быть полезно для чтения некоторых данных конфигурации или любого другого...

Таким образом, наследование от Card и BoardElement будет использоваться для реализации функций, открытых базовыми классами, а не для предоставления других методов, которые могут быть достигнуты только через dynamic_cast.

Для полноты:

class Player {
  void add(Card* card) {
    //..
    card->OnUserHands();
    //..
  }

  void useCard(Card* card) {
    //..

    //someway he got to retrieve the board...
    getBoard()->add(card->getBoardElement());

    //..
  }

  Card const* get_card();
};

class Board {
  void add(BoardElement* el) {
    //..
    el->OnBoard();
    //..
  }
};

Таким образом, у нас нет dynamic_cast, Player и доска делают простые вещи, не зная о внутренних деталях карты, которую они обрабатывают, обеспечивая хорошее разделение между различными объектами и увеличивающуюся ремонтопригодность.

Говоря о ActionCard и о "эффектах", которые могут быть применены к другим игрокам или вашему аватару, мы можем подумать о том, что у вас есть такой метод:

enum EffectTarget {
  MySelf,      //a player on itself, an enemy on itself
  MainPlayer,
  Opponents,
  StrongOpponents

  //....
};

class Effect {
public:
  //...
  virtual void Do(Target* target) = 0;
  //...
};

class Card {
public:
  //...
  struct Modifiers {
    EffectTarget eTarget;
    std::shared_ptr<Effect> effect;
  };

  virtual std::vector<Modifiers> getModifiers() { /*...*/ }

  //...
};

class Player : public Target {
public: 

  void useCard(Card* card) {
    //..

    //someway he got to retrieve the board...
    getBoard()->add(card->getBoardElement());

    auto modifiers = card->getModifiers();
    for each (auto modifier in modifiers)
    {
      //this method is supposed to look at the board, at the player and retrieve the instance of the target 
      Target* target = getTarget(modifier.eTarget);
      modifier.effect->Do(target);
    }

    //..
  }

};

Это еще один пример того же шаблона, чтобы применить эффекты от карты, избегая того, чтобы карты знали подробности о доске и ее статусе, кто играет на карточке, и держать код в Player довольно простым.

Надеюсь, это может помочь, Хороший день, Стефано.

Ответ 6

Что я создал плохо?

Проблема в том, что вам всегда нужно расширять этот код всякий раз, когда вводится новый тип Card.

Как я могу избежать dynamic_cast?

Обычный способ избежать этого - использовать интерфейсы (т.е. чистые абстрактные классы):

struct ICard {
   virtual bool can_put_on_board() = 0;
   virtual ~ICard() {}
};

class BoardCard : public ICard {
public:
    bool can_put_on_board() { return true; };
};

class ActionCard : public ICard {
public:
    bool can_put_on_board() { return false; };
};

Таким образом, вы можете просто использовать ссылку или указатель на ICard и проверить, если фактический тип он держит может быть поставлен на Board.


Но я не могу найти статью с соответствующим использованием (показывая хороший дизайн, а не только "как использовать").

В общем, я бы сказал, что для динамического броска нет хороших реальных случаев использования.

Иногда я использовал его в коде отладки для реализации CRTP, например

template<typename Derived> 
class Base {
public:
    void foo() {
#ifndef _DEBUG     
      static_cast<Derived&>(*this).doBar();
#else
      // may throw in debug mode if something is wrong with Derived
      // not properly implementing the CRTP
      dynamic_cast<Derived&>(*this).doBar();
#endif
    }
};

Ответ 7

Я думаю, что у меня получится что-то вроде этого (скомпилировано с clang 5.0 с -std = С++ 17). Я прошу про ваши комментарии. Поэтому всякий раз, когда я хочу обращаться с разными типами карточек, мне нужно создать экземпляр диспетчера и методы подачи с соответствующими сигнатурами.

#include <iostream>
#include <typeinfo>
#include <type_traits>
#include <vector>


template <class T, class... Args>
struct any_abstract {
    static bool constexpr value = std::is_abstract<T>::value || any_abstract<Args...>::value;
};


template <class T>
struct any_abstract<T> {
    static bool constexpr value = std::is_abstract<T>::value;
};


template <class T, class... Args>
struct StaticDispatcherImpl {
    template <class P, class U>
    static void dispatch(P* ptr, U* object) {
        if (typeid(*object) == typeid(T)) {
            ptr->do_dispatch(*static_cast<T*>(object));
            return;
        }

        if constexpr (sizeof...(Args)) {
            StaticDispatcherImpl<Args...>::dispatch(ptr, object);
        }
    }
};


template <class Derived, class... Args>
struct StaticDispatcher {
    static_assert(not any_abstract<Args...>::value);

    template <class U>
    void dispatch(U* object) {
        if (object) {
            StaticDispatcherImpl<Args...>::dispatch(static_cast<Derived *>(this), object);
        }
    }
};


struct Card {
    virtual ~Card() {}
};
struct BoardCard : Card {};
struct ActionCard : Card {};


struct Board {
    void put_card(BoardCard const& card, int const row, int const column) {
        std::cout << "Putting card on " << row << " " << column << std::endl;
    }
};


struct UI : StaticDispatcher<UI, BoardCard, ActionCard> {
    void do_dispatch(BoardCard const& card) {
        std::cout << "Get row to put: ";
        int row;
        std::cin >> row;

        std::cout << "Get row to put:";
        int column;
        std::cin >> column;

        board.put_card(card, row, column);
    }

    void do_dispatch(ActionCard& card) {
        std::cout << "Handling action card" << std::endl;
    }

private:
    Board board;
};


struct Game {};


int main(int, char**) {
    Card* card;
    ActionCard ac;
    BoardCard bc;

    UI ui;

    card = &ac;
    ui.dispatch(card);

    card = &bc;
    ui.dispatch(card);

    return 0;
}

Ответ 8

Поскольку я не понимаю, почему вы не будете использовать виртуальные методы, я просто собираюсь представить, как бы я это сделал. Сначала у меня есть интерфейс ICard для всех карт. Тогда я бы различал между типами карт (например, BoardCard и ActionCard и любыми картами, которые у вас есть). И все карты наследуются от одного из типов карт.

class ICard {
    virtual void put_card(Board* board) = 0;
    virtual void accept(CardVisitor& visitor) = 0; // See later, visitor pattern
}

class ActionCard : public ICard {
    void put_card(Board* board) final {
        // std::cout << "You can't put Action Cards on the board << std::endl;
        // Or just do nothing, if the decision of putting the card on the board
        // is not up to the user
    }
}

class BoardCard : public ICard {
    void put_card(Board* board) final {
        // Whatever implementation puts the card on the board, mb something like:
        board->place_card_on_board(this);
    }
}

class SomeBoardCard : public BoardCard {
    void accept(CardVisitor& visitor) final { // visitor pattern
        visitor.visit(this);
    }
    void print_information(); // see BaseCardVisitor in the next code section
}
class SomeActionCard : public ActionCard {
    void accept(CardVisitor& visitor) final { // visitor pattern
        visitor.visit(this);
    }
    void print_information(); // see BaseCardVisitor
}

class Board {
    void put_card(ICard* const card) {
         card->put_card(this);
    }

    void place_card_on_board(BoardCard* card) {
         // place it on the board
    }
}

Я предполагаю, что пользователь должен каким-то образом узнать, какую карту он нарисовал, поэтому для этого я бы использовал шаблон посетителя. Вы также можете поместить accept -method, который я разместил в наиболее производных классах/карточках, в типах карт (BoardCard, ActionCard), в зависимости от того, где вы хотите нарисовать линию, какая информация должна быть предоставлена пользователю.

template <class T>
class BaseCardVisitor {
    void visit(T* card) {
        card->print_information();
    }
}

class CardVisitor : public BaseCardVisitor<SomeBoardCard>,
                    public BaseCardVisitor<SomeActionCard> {

}

class Player {
    void add_card(ICard* card);
    ICard const* get_card();

    void what_is_this_card(ICard* card) {
          card->accept(visitor);
    }

    private:
      CardVisitor visitor;
};

Ответ 9

Вряд ли полный ответ, но он просто хотел ответить с ответом, похожим на Mark Ransom но, как правило, в общем, я нашел, что downcasting будет полезен в случаях, когда утиная печать действительно полезна. Могут быть определенные архитектуры, где очень полезно делать такие вещи:

for each object in scene:
{
     if object can fly:
          make object fly
}

Или же:

for each object in scene that can fly:
     make object fly

COM позволяет подобным образом выглядеть так:

for each object in scene:
{
     // Request to retrieve a flyable interface from
     // the object.
     IFlyable* flyable = object.query_interface<IFlyable>();

     // If the object provides such an interface, make
     // it fly.
     if (flyable)
          flyable->fly();
}

Или же:

for each flyable in scene.query<IFlyable>:
     flyable->fly();

Это подразумевает, что какая-то форма находится в централизованном коде для запроса и получения интерфейсов (например: от IUnknown до IFlyable). В таких случаях доступ к динамической проверке времени выполнения является самым безопасным типом приведения. Во-первых, может быть общая проверка, чтобы убедиться, что объект предоставляет интерфейс, который не включает в себя литье. Если это не так, эта функция query_interface может возвращать нулевой указатель или некоторый тип нулевого дескриптора/ссылки. Если это так, то использование dynamic_cast против RTTI - это самая безопасная вещь, чтобы сделать фактический указатель на общий интерфейс (например: IInterface*) и вернуть IFlyable* клиенту.

Другим примером являются системы сущностей. В этом случае вместо запроса абстрактных интерфейсов мы извлекаем конкретные компоненты (данные):

Flight System:
for each object in scene:
{
     if object.has<Wings>():
          make object fly using object.get<Wings>()
}

Или же:

for each wings in scene.query<Wings>()
     make wings fly

... что-то по этому поводу, и это также подразумевает кастинг.

enter image description here

Для моего домена (VFX, который несколько похож на игры с точки зрения приложения и состояния сцены), я нашел этот тип архитектуры ECS самым легким в обслуживании. Я могу говорить только из личного опыта, но я долгое время встречался и сталкивался со многими различными архитектурами. COM теперь самый популярный стиль архитектуры в VFX, и я работал над коммерческим VFX-приложением, широко используемым в фильмах и играх и архивизе и т.д., Которые использовали COM-архитектуру, но я нашел ECS популярным в игровых машинах даже легче поддерживать, чем COM для моего конкретного случая *.

  • Одна из причин, по которой я нахожу ECS намного проще, заключается в том, что основная часть систем в этой области, таких как PhysicsSystem, RenderingSystem, AnimationSystem и т. PhysicsSystem, PhysicsSystem только к трансформаторам данных, а модель ECS просто подходит для этой цели без абстракций, путь. С COM в этом домене количество подтипов, реализующих интерфейс, такой как интерфейс движения, как IMotion может быть в сотнях (например: PointLight который реализует IMotion вместе с 5 другими интерфейсами), требуя от сотен классов, реализующих различные комбинации COM-интерфейсов, поддерживать индивидуально. С ECS он использует композиционную модель над наследованием и уменьшает эти сотни классов до нескольких десятков простых компонентных structs которые могут быть объединены бесконечными способами сущностями, которые их составляют, и только несколько систем должны предоставить поведение: все остальное - это просто данные, которые системы циклируют в качестве входных данных, а затем предоставляют некоторый результат.

Между устаревшими кодовыми базами, которые использовали кучу глобальных переменных и кодировку грубой силы (например: условные условия разбрызгивания повсюду вместо использования полиморфизма), иерархии глубокого наследования, COM и ECS, с точки зрения ремонтопригодности для моего конкретного домена, я бы например, ECS > COM, в то время как иерархии глубокого наследования и кодировка грубой силы с глобальными переменными повсюду были невероятно трудными для поддержания (ООП, использующий глубокое наследование с защищенными полями данных, почти так же трудно объяснить с точки зрения сохранения инвариантов как лодка глобальных переменных IMO, но в дальнейшем может предложить самые кошмарные каскадные изменения, разливающиеся по целым иерархиям, если проекты должны измениться - по крайней мере, у устаревшей кодовой базы грубой силы не было проблемы с каскадом, поскольку она почти не использовала какой-либо код для начала),

COM и ECS несколько похожи, кроме COM, поток зависимостей IFlyable к центральным абстракциям (COM-интерфейсы, предоставляемые COM-объектами, например IFlyable). С ECS зависимости поступают к центральным данным (компоненты, предоставляемые объектами ECS, например Wings). В основе обеих причин часто лежит идея о том, что у нас есть куча неоднородных объектов (или "сущностей"), представляющих интерес, предоставляемые интерфейсы или компоненты не известны заранее, поскольку мы обращаемся к ним через неоднородную коллекцию (например: "Сцена"). В результате нам нужно обнаружить их возможности во время выполнения при повторении через эту неоднородную коллекцию путем либо запроса коллекции, либо объектов в отдельности, чтобы увидеть, что они предоставляют.

В любом случае, оба включают в себя некоторый тип централизованного каста для извлечения интерфейса или компонента из объекта, а если нам нужно отключить, то dynamic_cast - это, по крайней мере, самый безопасный способ сделать это, что включает проверку типа времени выполнения, чтобы убедиться, что листинг действителен. И как с ECS, так и с COM, вам обычно нужна только одна строка кода во всей системе, которая выполняет этот приведение.

Тем не менее, проверка времени выполнения имеет небольшую стоимость. Обычно, если dynamic_cast используется в архитектурах COM и ECS, это делается так, что std::bad_cast никогда не должен быть брошен и/или что dynamic_cast сам никогда не возвращает nullptr (dynamic_cast - это просто проверка работоспособности, чтобы убедиться, что нет внутренние ошибки программиста, а не как способ определить, наследует ли объект тип). Чтобы избежать этого, выполняется другой тип проверки времени выполнения (например: один раз для всего запроса в ECS при извлечении всех компонентов PosAndVelocity для определения того, какой список компонентов использовать, который на самом деле является однородным и только сохраняет компоненты PosAndVelocity). Если эта небольшая стоимость исполнения не является незначительной, потому что вы зацикливаете лодку компонентов на каждый кадр и выполняете тривиальную работу для каждого, то я нашел этот фрагмент полезным из Herb Sutter в C++ Coding Standards:

template<class To, class From> To checked_cast(From* from) {
    assert( dynamic_cast<To>(from) == static_cast<To>(from) && "checked_cast failed" );
    return static_cast<To>(from);
}

template<class To, class From> To checked_cast(From& from) {
    assert( dynamic_cast<To>(from) == static_cast<To>(from) && "checked_cast failed" );
    return static_cast<To>(from);
}

Он в основном использует dynamic_cast в качестве проверки работоспособности для отладочных сборников с поддержкой assert и static_cast для выпуска.