c++ pointers design reference

c++ - Caso de uso de dynamic_cast



pointers design (9)

En muchos lugares, puedes leer que 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 cartas descritas con muchos atributos (algunas tarjetas pueden colocarse 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 mesa directiva '' put_card(BoardCard const&) - es parte de la interfaz que no puedo poner ninguna carta en la pizarra. Si tuviera solo un tipo de tarjeta, debería verificarlo dentro del método.

Veo el flujo de la siguiente manera:

  • una tarjeta genérica está en el mazo (no es importante cuál es su tipo)
  • una carta genérica es sacada de la baraja y entregada a un jugador (igual que la anterior)
  • si un jugador elige una BoardCard, entonces puede colocarla en el tablero

Así que 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 ? ¿Usar algún tipo de atributo y if s sería una mejor solución ...?

PD: cualquier fuente que trate sobre el uso de dynamic_cast en el contexto del diseño es más que apreciada.


¿Qué he diseñado mal?

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

¿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 puntero a ICard y verificar, si el tipo real que contiene se puede poner en la 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 } };


¿Por qué no usar dynamic_cast

dynamic_cast generalmente no le gusta porque se puede abusar fácilmente para romper completamente 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, la Board no debería haber put_card método put_card , tal vez en lugar de la tarjeta debería tener el método play(const PlaySpace *) donde Board implemente PlaySpace o algo así. Incluso CppCoreGuidelines desaconseja el uso de 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 ( auto_ptr cuenta el auto_ptr prehistórico): http://www.drdobbs.com/double-dispatch-revisited/184405527

También Scott Meyers en uno de sus libros escribió algo acerca de construir una matriz de despacho doble con dynamic_cast . Pero, en general, estos dynamic_cast s 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

Mira el patrón de visitante . Se puede usar como reemplazo de dynamic_cast pero también es un olor a código.

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


Apenas una respuesta completa, pero solo quería ofrecer una respuesta similar a la de Mark Ransom pero, hablando muy en términos generales, he encontrado que downcasting es útil en los casos en que la tipificación de 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: de 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, esta función query_interface puede devolver un puntero nulo o algún tipo de manejador / referencia nulo. Si lo hace, entonces usar un dynamic_cast contra RTTI es lo más seguro que se puede hacer para obtener el puntero real a la interfaz genérica (por ejemplo: IInterface* ) y devolver 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.

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 la mayoría de los sistemas en este dominio como PhysicsSystem , RenderingSystem , AnimationSystem , etc. se reducen a solo transformadores de datos y el modelo ECS simplemente encaja perfectamente para ese propósito sin abstracciones 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 ser de cientos (por ejemplo, un PointLight que implementa IMotion junto con otras 5 interfaces), requiriendo cientos de clases implementando diferentes combinaciones de interfaces COM para mantener 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 structs componentes simples que pueden combinarse de manera infinita por las entidades que las componen, y solo un puñado de sistemas debe proporcionar comportamiento: todo lo demás no es más que datos a los que los sistemas pasan como una entrada para 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, digamos ECS > COM , mientras que las jerarquías de herencia profunda y la fuerza bruta con variables globales por 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 una carga de barco de las 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 que 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 dynamic_cast , 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 asegurarnos de que el yeso es 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. Normalmente, si se utiliza dynamic_cast en las arquitecturas COM y ECS, se hace de una manera tal que std::bad_cast nunca se std::bad_cast y / o que dynamic_cast nunca devuelva nullptr (el dynamic_cast sea ​​solo una verificación de cordura para asegurarse de que no hay 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 PosAndVelocity todos los componentes de PosAndVelocity para determinar qué lista de componentes usar, que es realmente homogénea y solo almacena componentes PosAndVelocity ). 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, utiliza dynamic_cast como una comprobación de cordura para las compilaciones de depuración con un assert y static_cast para static_cast de lanzamiento.


Como no puedo ver por qué no usarías los métodos virtuales, solo voy a presentar cómo lo haría. Primero tengo la interfaz ICard para todas las tarjetas. Luego distinguiría, entre los tipos de tarjetas (es decir, BoardCard y ActionCard y cualquier tarjeta 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 puede colocar el método de accept , que coloqué en las clases / tarjetas más derivadas, en los tipos de tarjeta (BoardCard, ActionCard), dependiendo de dónde desea trazar la línea de qué información se le 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; };


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


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 algo que hace una carta , 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 una unión etiquetada : deje que Card sea ​​una std::variant<BoardCard, ActionCard...> y contenga una instancia del tipo apropiado. Al decidir qué hacer con la tarjeta, utiliza un switch en el index() y luego std::get<> solo el tipo apropiado. De esta forma, no necesita ningún *_cast operador *_cast , y obtiene una completa libertad de qué métodos (ninguno de los cuales tendría sentido para los otros tipos) admite cada tipo de tarjeta.

Si solo es casi cierto, pero no para todos los tipos, puede variar levemente: solo agrupe aquellos tipos de tarjetas que pueden superclasarse de forma sensata y coloque el conjunto de esos tipos comunes en la variant .


Podrías aplicar los principios detrás del COM de Microsoft y proporcionar una serie de interfaces , con cada interfaz describiendo un conjunto de comportamientos relacionados. En COM se determina si una interfaz específica está disponible llamando a 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.


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 . Me can_put_on_board a decir que can_put_on_board es peor: estás duplicando el código implementado de otra manera por dynamic_cast y dynamic_cast 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. Por ejemplo, agregue una función de play(Context& c) a la tarjeta (el Context es 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:

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

Otros ejemplos de por qué se debe evitar la herencia y cómo lograr el polimorfismo en tiempo de ejecución; de lo contrario, es posible que desee ver la excelente charla de Sean Parent "Herencia es la clase base del mal" . Una biblioteca de aspecto prometedor que implementa este tipo de polimorfismo es dyno , aunque 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 implementar comportamientos para diferentes categorías, por ejemplo, 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 tu Goblin como:

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

lo 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 función de play diferente.

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.


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 se omiten muchos detalles 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 más 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 extienden 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 añadir que este patrón permite muchos trucos dentro del método getBoardElement() , actuar como una fábrica (por lo que algo debe generarse con su propia vida útil), devolver un miembro de datos de la Card como std:shared_ptr<BoardElement> wizard3D; (como ejemplo), cree un enlace entre la Card y BoardElement cuanto a:

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

Por lo tanto, la herencia de Card y BoardElement se utilizará para implementar las características expuestas por las clases base y no para proporcionar otros métodos a los que solo se pueda acceder 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 la ActionCard y de 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, evitando que las tarjetas conozcan detalles sobre la placa y su estado, quién está jugando la carta y mantener el código en Player bastante simple.

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