Pregunta Caso de uso de dynamic_cast


En muchos lugares puedes leer eso dynamic_cast significa "mal diseño". Pero no puedo encontrar ningún artículo con el uso apropiado (que muestre un buen diseño, no solo "cómo usarlo").

Estoy escribiendo un juego de mesa con un tablero y muchos tipos diferentes de tarjetas descritas con muchos atributos (algunas cartas se pueden poner en el tablero). Así que decidí dividirlo en las siguientes clases / interfaces:

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*);
};

Algunos chicos sugirieron que debería usar solo una clase que describa una carta. Pero me referiría a muchos atributos mutuamente excluyentes. Y en el caso de la clase de la Junta " put_card(BoardCard const&) - Es parte de la interfaz que no puedo poner ninguna tarjeta en el tablero. Si tuviera solo un tipo de tarjeta, debería verificarlo dentro del método.

Veo el flujo de la siguiente manera:

  • un genérico la tarjeta está en la baraja (no es importante cuál es su tipo)
  • un genérico la carta se saca de la baraja y se le da a un jugador (lo mismo que arriba)
  • si un jugador elegido a BoardCard entonces se puede poner en el tablero

Entonces yo uso dynamic_cast antes de poner una carta en el tablero. Creo que usar un método virtual está fuera de cuestión en este caso (además, no tendría ningún sentido agregar alguna acción sobre el tablero a cada carta).

Entonces mi pregunta es: ¿qué he diseñado mal? Cómo podría evitar dynamic_cast? Usando algún tipo de atributo y ifs sería una mejor solución ...?

PD Cualquier fuente que trate sobre dynamic_cast el uso en el contexto del diseño es más que apreciado.


25
2018-02-04 19:41


origen


Respuestas:


Sí, dynamic_cast es un olor a código, pero también lo es agregar funciones que intentan hacer que parezca que tienes una buena interfaz polimórfica, pero en realidad equivalen a un dynamic_cast es decir, cosas como can_put_on_board. Yo iría tan lejos como para decir eso can_put_on_board es peor - está duplicando código implementado de otra manera por dynamic_cast y abarrotando la interfaz.

Al igual que con todos los olores de código, deberían desconfiar y no necesariamente significan que tu código es malo. Todo esto depende de lo que estás tratando de lograr.

Si estás implementando un juego de mesa que tendrá 5k líneas de código, dos categorías de cartas, entonces todo lo que funcione está bien. Si está diseñando algo más grande, extensible y posiblemente permitiendo tarjetas creadas por no programadores (ya sea que se trate de una necesidad real o lo esté haciendo para investigación), probablemente esto no funcionará.

Suponiendo lo último, veamos algunas alternativas.

Puede poner la carga de aplicar la tarjeta correctamente a la tarjeta, en lugar de un código externo. P.ej. Agrega un play(Context& c) función a la tarjeta (el Context ser un medio para acceder al tablero y lo que sea necesario). Una tarjeta de la tabla sabría que solo se puede aplicar a una pizarra y no sería necesario un yeso.

Sin embargo, renunciaría por completo al uso de la herencia. Uno de sus muchos problemas es cómo introduce una categorización de todas las tarjetas. Dejame darte un ejemplo:

  • usted introduce BoardCard y ActionCard poniendo todas las cartas en estos dos cubos;
  • luego decides que quieres tener una tarjeta que se puede usar de dos maneras, ya sea como Action o una Board tarjeta;
  • digamos que resolvió el problema (a través de la herencia múltiple, una BoardActionCard tipo, o de cualquier manera diferente);
  • luego decides que quieres tener los colores de la tarjeta (como en MtG), ¿cómo lo haces? ¿Creas? RedBoardCard, BlueBoardCard, RedActionCard etc?

Otros ejemplos de por qué se debe evitar la herencia y cómo lograr el polimorfismo de tiempo de ejecución; de lo contrario, es posible que desee ver la excelente educación de Sean Parent La "herencia es la clase base del mal". Una biblioteca de aspecto prometedor que implementa este tipo de polimorfismo es dynoSin embargo, aún no lo he probado.

Una posible solución podría ser:

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_;

};

Luego puede crear clases por tipo de tarjeta:

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

O implemente comportamientos para diferentes categorías, p. tener un

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

plantilla y luego use enable_if para manejarlo para diferentes categorías:

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

dónde:

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

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

luego definiendo su Goblin como:

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

que le permitiría categorizar sus tarjetas en muchas dimensiones, dejando también la posibilidad de especializar por completo el comportamiento mediante la implementación de una diferente play función.

El código de ejemplo usa std :: shared_ptr para almacenar el modelo, pero definitivamente puede hacer algo más inteligente aquí. Me gusta usar un almacenamiento de tamaño estático y solo permitir que se usen Ts de cierto tamaño máximo y alineación. Alternativamente, podría usar std :: unique_ptr (que no permitiría la copia) o una variante aprovechando la optimización de tamaño pequeño.


13
2018-02-06 10:28



Podrías aplicar los principios detrás COM de Microsoft y proporcionar una serie de interfaces, con cada interfaz que describe un conjunto de comportamientos relacionados. En COM, usted determina si hay una interfaz específica disponible llamando QueryInterface, pero en C ++ moderno dynamic_cast funciona de manera similar y es más eficiente.

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

Ese puede ser un mal ejemplo, pero espero que entiendas la idea.


3
2018-02-05 05:36



Por qué no usar dynamic_cast

dynamic_cast En general, no le agradan porque se puede abusar fácilmente para romper por completo las abstracciones utilizadas. Y no es prudente depender de implementaciones específicas. Por supuesto que puede ser necesario, pero muy rara vez, por lo que casi todo el mundo toma una regla general: probablemente no deberías usarla. Es un olor codificado que puede implicar que debes reconsiderar tus abstracciones porque pueden no ser las que necesitas en Tu dominio. Tal vez en tu juego el Board no debería tener put_card método - tal vez la tarjeta debe tener el método play(const PlaySpace *) dónde Board implementos PlaySpace o algo así. Incluso CppCoreGuidelines desaconseja usar dynamic_cast en la mayoría de los casos.

Cuando uso

En general, pocas personas tienen problemas como este, pero ya lo he visto varias veces. El problema se llama Despacho doble (o múltiple). Aquí está bastante viejo, pero es un artículo bastante relevante sobre el doble despacho (tenga en cuenta lo prehistórico auto_ptr) http://www.drdobbs.com/double-dispatch-revisited/184405527

También Scott Meyers en uno de sus libros escribió algo sobre construir una matriz de despacho doble con dynamic_cast. Pero, en general, estos dynamic_casts están "ocultos" dentro de esta matriz: los usuarios no saben qué tipo de magia sucede dentro.

Digno de mención: el envío múltiple también se considera olor a código :-).

Alternativa razonable

Revisar la patrón de visitante. Se puede usar como reemplazo de dynamic_cast pero también es algún tipo de olor a código.

Generalmente recomiendo usar dynamic_cast y el visitante como herramientas de último recurso para problemas de diseño, ya que rompen la abstracción, lo que aumenta la complejidad.


3
2018-02-09 21:04



Me parece que los dos tipos de cartas son bastante diferentes. Las cosas que pueden hacer una carta de tablero y una carta de acción son mutuamente excluyentes, y lo común es que puedan ser sorteadas desde el mazo. Además, eso no es una cosa tarjeta lo hace, es una acción de jugador / mazo.

Si esto es cierto, una pregunta que debería hacerse es si realmente deberían descender de un tipo común, Card. Un diseño alternativo sería el de un unión etiquetada: dejar Card en su lugar ser un std::variant<BoardCard, ActionCard...>y Contiene una instancia del tipo apropiado. Al decidir qué hacer con la tarjeta, usa una switch sobre el index() y entonces std::get<> solo el tipo apropiado. De esta manera no necesitas alguna  *_cast operador, y obtener una completa libertad de qué métodos (ninguno de los cuales tendría sentido para los otros tipos) admite cada tipo de tarjeta.

Si es solo casi cierto, pero no para todos los tipos, puede variar levemente: solo agrupe aquellos tipos de tarjetas que se pueden superclasificar con sensatez, y coloque el conjunto de esos tipos comunes en el variant.


2
2018-02-09 08:31



Siempre me pareció que el uso de un molde tiene un olor a código, y en mi experiencia, el 90% de las veces el molde se debió a un mal diseño. Vi el uso de dynamic_cast en alguna aplicación de tiempo crítico en la que proporcionaba más mejoras en el rendimiento que heredar de múltiples interfaces o recuperar una enumeración de algún tipo del objeto (como un tipo). Entonces el código olía, pero el uso del molde dinámico valió la pena en ese caso.

Dicho esto, evitaré el lanzamiento dinámico en su caso, así como herencias múltiples de diferentes interfaces.

Antes de llegar a mi solución, su descripción parece que hay muchos detalles omitidos sobre el comportamiento de las cartas o las consecuencias que tienen en la pizarra y el juego en sí Lo usé como una restricción adicional, tratando de mantener cosas en caja y mantenibles.

Preferiría una composición en lugar de una herencia. Le brindará la oportunidad de utilizar la tarjeta como una 'fábrica' de manera uniforme:

  • puede engendrar más modificadores del juego, algo que se aplicará al tablero, y uno a un enemigo específico
  • la tarjeta se puede reutilizar: la carta podría quedarse en manos del jugador y el efecto en el juego se despegará de ella (no hay empate 1-1 entre cartas y efectos)
  • la carta en sí misma puede sentarse en el mazo, mientras que los efectos de lo que hizo aún están vivos en el tablero.
  • una carta puede tener una representación (métodos de dibujo) y reaccionar al tacto de una manera, donde el BoardElement puede ser una miniatura en 3D con animación

Ver [https://en.wikipedia.org/wiki/Composition_over_inheritance para mas detalles]. Me gustaría citar: La composición también proporciona un dominio comercial más estable a largo plazo, ya que es menos propenso a las peculiaridades de los miembros de la familia. En otras palabras, es mejor componer lo que un objeto puede hacer (HAS-A) que extender lo que es ( IS - A). [1]

Una BoardCard / Element puede ser algo como esto:

//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's 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() {}
};

la Tarjeta podría representar algo para ser utilizado en un mazo o en las manos del usuario, agregaré una interfaz de ese tipo

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() {}
};

Me gustaría agregar que este patrón permite muchos trucos dentro del getBoardElement() método, de actuar como una fábrica (por lo que algo debe generarse con su propia vida), devolviendo un Card miembro de datos como un std:shared_ptr<BoardElement> wizard3D; (como ejemplo), crea un enlace entre Card y el BoardElement como para:

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

  // other members omitted ...
};

El enlace puede ser útil para leer algunos datos de configuración o lo que sea ...

Así que la herencia de Card y de BoardElement se usará para implementar las características expuestas por las clases base y no para proporcionar otros métodos que solo se pueden alcanzar a través de un dynamic_cast.

Por completitud:

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

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

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

    //..
  }

  Card const* get_card();
};

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

De esa forma, no tenemos dynamic_cast, Player y Board hacen cosas simples sin conocer los detalles internos de la tarjeta que se manejan, proporcionando buenas separaciones entre los diferentes objetos y aumentando el mantenimiento.

Hablando de ActionCardy sobre los "efectos" que pueden aplicarse a otros jugadores o a tu avatar, podemos pensar en tener un método como:

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's 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);
    }

    //..
  }

};

Ese es otro ejemplo del mismo patrón para aplicar los efectos de la tarjeta, evitar que las tarjetas conozcan los detalles sobre la placa y su estado, quién está jugando la carta y mantener el código en la tarjeta. Player bastante simple.

Espero que esto ayude, Que tengas un buen día, Stefano.


1
2018-02-10 11:24



¿Qué he diseñado mal?

El problema es que siempre necesita extender ese código cada vez que un nuevo tipo de Card es presentado.

¿Cómo podría evitar dynamic_cast?

La forma habitual de evitar eso es utilizar interfaces (es decir, clases abstractas puras):

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; };
};

De esta manera, simplemente puede usar una referencia o un puntero para ICard y verifique, si el tipo real que contiene se puede poner en el Board.


Pero no puedo encontrar ningún artículo con el uso apropiado (que muestre un buen diseño, no solo "cómo usarlo").

En general, diría que no hay buenos casos de uso de la vida real para el reparto dinámico.

A veces lo he usado en el código de depuración para realizaciones de CRTP como

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
    }
};

0
2018-02-04 19:51



Creo que terminaría con algo así (compilado con clang 5.0 con -std = c ++ 17). Estoy couroius acerca de tus comentarios. Entonces, cuando quiera manejar diferentes tipos de tarjetas, necesito instanciar un despachador y suministrar métodos con firmas apropiadas.

#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;
}

0
2018-02-08 20:23



Como no puedo ver por qué no usarías los métodos virtuales, solo voy a presentar cómo lo haría. Primero tengo el ICard interfaz para todas las tarjetas. Luego distinguiría, entre los tipos de tarjetas (es decir, BoardCard y ActionCard y las cartas que tenga). Y todas las tarjetas heredan de cualquiera de los tipos de tarjeta.

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
    }
}

Supongo que el usuario debe saber de alguna manera qué tarjeta ha dibujado, así que para eso implementaría el patrón de visitante. También puedes colocar el accept-metodo, que ubiqué en las clases / tarjetas más derivadas, en los tipos de tarjetas (BoardCard, ActionCard), dependiendo de dónde quiera trazar la línea sobre qué información se le debe dar al usuario.

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;
};

0
2018-02-09 11:44



Apenas una respuesta completa pero solo quería lanzar una respuesta similar a Mark Ransom's, pero en términos muy generales, he encontrado downcasting para ser útil en los casos en que la mecanografía pato es realmente útil. Puede haber ciertas arquitecturas donde es muy útil hacer cosas como esta:

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

O:

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

COM permite este tipo de cosas algo así como:

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();
}

O:

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

Esto implica un molde de alguna forma en algún lugar del código centralizado para consultar y obtener interfaces (por ejemplo: desde IUnknown a IFlyable) En tales casos, una información dinámica del tipo de tiempo de ejecución de comprobación es el tipo más seguro de conversión disponible. En primer lugar, puede haber una comprobación general para ver si un objeto proporciona la interfaz que no involucra el casting. Si no lo hace, esto query_interface la función puede devolver un puntero nulo o algún tipo de manejador / referencia nulo. Si lo hace, entonces usando un dynamic_cast contra RTTI es lo más seguro que hay que hacer para obtener el puntero real a la interfaz genérica (por ejemplo: IInterface*) y volver IFlyable* al cliente.

Otro ejemplo es sistemas de componentes de entidad. En ese caso, en lugar de consultar interfaces abstractas, recuperamos componentes concretos (datos):

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

O:

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

... algo en este sentido, y eso también implica lanzar en algún lado.

enter image description here

Para mi dominio (VFX, que es algo similar a los juegos en términos de aplicación y estado de escena), he encontrado que este tipo de arquitectura ECS es la más fácil de mantener. Solo puedo hablar por experiencia personal, pero he estado presente durante mucho tiempo y he enfrentado muchas arquitecturas diferentes. COM es ahora el estilo de arquitectura más popular en VFX y solía trabajar en una aplicación VFX comercial utilizada ampliamente en películas y juegos y archviz, etc., que utilizaba una arquitectura COM, pero he encontrado a ECS tan popular en los motores de juegos incluso más fácil de mantener que COM para mi caso particular *.

  • Una de las razones por las que considero que ECS es mucho más fácil es porque a la mayoría de los sistemas en este dominio les gusta PhysicsSystem, RenderingSystem, AnimationSystem, etc. se reduce a solo transformadores de datos y el modelo de ECS simplemente encaja muy bien para ese propósito sin que las abstracciones se interpongan en el camino. Con COM en este dominio, la cantidad de subtipos que implementan una interfaz como una interfaz de movimiento como IMotion podría estar en los cientos (por ejemplo: a PointLight que implementa IMotion junto con otras 5 interfaces), que requieren cientos de clases implementando diferentes combinaciones de interfaces COM para mantenerlas individualmente. Con ECS, utiliza un modelo de composición sobre la herencia y reduce esos cientos de clases a solo un par de docenas de componentes simples. structsque pueden combinarse de manera interminable por las entidades que los componen, y solo un puñado de sistemas tienen que proporcionar un comportamiento: todo lo demás son solo datos a los que los sistemas recurren como aportes para luego proporcionar algún resultado.

Entre las bases de códigos heredadas que usaban un conjunto de variables globales y la codificación de fuerza bruta (por ejemplo, rociar condicionales por todo el lugar en lugar de usar polimorfismo), jerarquías de herencia profunda, COM y ECS, en términos de facilidad de mantenimiento para mi dominio particular, decir ECS > COM, mientras que las profundas jerarquías de herencia y la codificación de fuerza bruta con variables globales en todas partes fueron increíblemente difíciles de mantener (OOP usando herencia profunda con campos de datos protegidos es casi tan difícil de razonar en términos de mantener invariantes como un bote de variables globales IMO , pero además puede invitar a la mayoría de los cambios en cascada de pesadilla que se extienden a través de jerarquías enteras si los diseños tienen que cambiar; al menos, la base de código heredada de fuerza bruta no tenía el problema en cascada ya que apenas se reutilizaba ningún código).

COM y ECS son algo similares, excepto con COM, las dependencias fluyen hacia abstracciones centrales (interfaces COM proporcionadas por objetos COM, como IFlyable) Con un ECS, las dependencias fluyen hacia datos centrales (componentes proporcionados por entidades ECS, como Wings) En el corazón de ambos, a menudo se encuentra la idea de que tenemos un grupo de objetos no homogéneos (o "entidades") de interés cuyas interfaces o componentes proporcionados no se conocen de antemano, ya que estamos accediendo a ellos a través de una colección no homogénea. (Ej: una "Escena"). Como resultado, tenemos que descubrir sus capacidades en tiempo de ejecución al iterar a través de esta colección no homogénea al consultar la colección o los objetos individualmente para ver lo que proporcionan.

De cualquier manera, ambos implican algún tipo de conversión centralizada para recuperar una interfaz o un componente de una entidad, y si tenemos que bajar, entonces un dynamic_cast es al menos la forma más segura de hacer eso, lo que implica la verificación del tipo de tiempo de ejecución para asegurarse de que el lanzamiento sea válido. Y con ECS y COM, generalmente solo necesita una línea de código en todo el sistema que realiza este reparto.

Dicho esto, la verificación en tiempo de ejecución tiene un costo bajo. Típicamente si dynamic_cast se usa en las arquitecturas COM y ECS, está hecho de una manera tal que std::bad_cast nunca debería ser arrojado y / o eso dynamic_cast en si nunca regresa nullptr (el dynamic_cast es solo un control de cordura para asegurarse de que no haya errores internos del programador, no como una forma de determinar si un objeto hereda un tipo). Se realiza otro tipo de control en tiempo de ejecución para evitar eso (por ejemplo, solo una vez para una consulta completa en un ECS cuando se busca todo PosAndVelocity componentes para determinar qué lista de componentes usar, que es realmente homogénea y solo almacena PosAndVelocity componentes). Si ese pequeño costo de tiempo de ejecución no es despreciable porque está recorriendo una gran cantidad de componentes en cada cuadro y haciendo un trabajo trivial para cada uno, entonces encontré este fragmento útil de Herb Sutter en 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);
}

Básicamente usa dynamic_cast como un control de cordura para compilaciones de depuración con una asserty static_cast para versiones de lanzamiento.


0
2018-02-13 05:12



Preguntas populares