c++ - ¿Uso práctico de dynamic_cast?
casting rtti (9)
Tengo una pregunta bastante simple sobre el operador dynamic_cast
. Sé que esto se usa para la identificación del tipo de tiempo de ejecución, es decir, para conocer el tipo de objeto en el tiempo de ejecución. Pero a partir de su experiencia en programación, ¿puede dar un escenario real en el que tuvo que usar este operador? ¿Cuáles fueron las dificultades sin usarlo?
Debe evitarse el lanzamiento cuando sea posible, ya que básicamente le dice al compilador que usted sabe mejor y por lo general es un signo de una decisión de diseño más débil.
Sin embargo, puede aparecer en situaciones donde el nivel de abstracción era demasiado alto para 1 o 2 subclases, donde tiene la opción de cambiar su diseño o resolverlo al verificar la subclase con dynamic_cast y manejarla en una rama separada. El intercambio es entre agregar tiempo adicional y riesgo ahora contra problemas de mantenimiento adicionales más adelante.
El operador dynamic_cast es muy útil para mí. Especialmente lo uso con el patrón Observer para la gestión de eventos :
#include <vector>
#include <iostream>
using namespace std;
class Subject; class Observer; class Event;
class Event { public: virtual ~Event() {}; };
class Observer { public: virtual void onEvent(Subject& s, const Event& e) = 0; };
class Subject {
private:
vector<Observer*> m_obs;
public:
void attach(Observer& obs) { m_obs.push_back(& obs); }
public:
void notifyEvent(const Event& evt) {
for (vector<Observer*>::iterator it = m_obs.begin(); it != m_obs.end(); it++) {
if (Observer* const obs = *it) {
obs->onEvent(*this, evt);
}
}
}
};
// Define a model with events that contain data.
class MyModel : public Subject {
public:
class Evt1 : public Event { public: int a; string s; };
class Evt2 : public Event { public: float f; };
};
// Define a first service that processes both events with their data.
class MyService1 : public Observer {
public:
virtual void onEvent(Subject& s, const Event& e) {
if (const MyModel::Evt1* const e1 = dynamic_cast<const MyModel::Evt1*>(& e)) {
cout << "Service1 - event Evt1 received: a = " << e1->a << ", s = " << e1->s << endl;
}
if (const MyModel::Evt2* const e2 = dynamic_cast<const MyModel::Evt2*>(& e)) {
cout << "Service1 - event Evt2 received: f = " << e2->f << endl;
}
}
};
// Define a second service that only deals with the second event.
class MyService2 : public Observer {
public:
virtual void onEvent(Subject& s, const Event& e) {
// Nothing to do with Evt1 in Service2
if (const MyModel::Evt2* const e2 = dynamic_cast<const MyModel::Evt2*>(& e)) {
cout << "Service2 - event Evt2 received: f = " << e2->f << endl;
}
}
};
int main(void) {
MyModel m; MyService1 s1; MyService2 s2;
m.attach(s1); m.attach(s2);
MyModel::Evt1 e1; e1.a = 2; e1.s = "two"; m.notifyEvent(e1);
MyModel::Evt2 e2; e2.f = .2f; m.notifyEvent(e2);
}
En la mayoría de las situaciones en las que está escribiendo código en el que conoce el tipo de entidad con la que está trabajando, simplemente use static_cast para que sea más eficiente.
Las situaciones en las que necesita un reparto dinámico suelen llegar (según mi experiencia) debido a la falta de previsión en el diseño; por lo general, cuando el diseñador no proporciona una enumeración o identificación que le permita determinar el tipo más adelante en el código.
Por ejemplo, ya he visto esta situación en más de un proyecto:
Puede usar una fábrica donde la lógica interna decida qué clase derivada desea el usuario en lugar de que el usuario la seleccione explícitamente. Esa fábrica, en un mundo perfecto, devuelve una enumeración que le ayudará a identificar el tipo de objeto devuelto, pero si no es así, es posible que tenga que probar qué tipo de objeto le dio con dynamic_cast.
Su pregunta de seguimiento obviamente sería: ¿Por qué necesitaría saber el tipo de objeto que está utilizando en el código usando una fábrica?
En un mundo perfecto, no lo haría: la interfaz proporcionada por la clase base sería suficiente para administrar todos los objetos devueltos de las fábricas a todas las extensiones requeridas. Sin embargo, la gente no diseña perfectamente. Por ejemplo, si su fábrica crea objetos de conexión abstractos, puede darse cuenta repentinamente de que necesita acceder al indicador UseSSL en su objeto de conexión de zócalo, pero la base de fábrica no lo admite y no es relevante para ninguna de las otras clases que usan el interfaz. Por lo tanto, tal vez debería verificar si está utilizando ese tipo de clase derivada en su lógica y activar / establecer la bandera directamente si lo está.
Es feo, pero no es un mundo perfecto, y a veces no tienes tiempo para refactorizar un diseño imperfecto completamente en el mundo real bajo la presión del trabajo.
Imagina esta situación: tienes un programa en C ++ que lee y muestra HTML. Tiene una clase base HTMLElement
que tiene un método virtual puro displayOnScreen
. También tiene una función llamada renderHTMLToBitmap
, que dibuja el HTML en un mapa de bits. Si cada HTMLElement
tiene un vector<HTMLElement*> children;
, simplemente puede pasar el HTMLElement
representa el elemento <html>
. Pero qué pasa si algunas de las subclases necesitan un tratamiento especial, como <link>
para agregar CSS. Necesita una forma de saber si un elemento es un LinkElement
para que pueda asignarlo a las funciones CSS. Para averiguarlo, dynamic_cast
.
El problema con dynamic_cast
y el polimorfismo en general es que no es muy eficiente. Cuando agregas vtables a la mezcla, solo empeora.
Cuando agregas funciones virtuales a una clase base, cuando se llaman, terminas atravesando bastantes capas de punteros de función y áreas de memoria. Eso nunca será más eficiente que algo como la instrucción de call
ASM.
Edición: En respuesta al comentario de Andrew, aquí hay un nuevo enfoque: en lugar de la conversión dinámica al tipo de elemento específico ( LinkElement
), en su lugar, tiene otra subclase abstracta de HTMLElement
llamada ActionElement
que reemplaza a displayOnScreen
con una función que no muestra nada y crea una nueva función virtual pura: virtual void doAction() const = 0
. El dynamic_cast
se cambia para probar ActionElement
y solo llama a doAction()
. Tendría el mismo tipo de subclase para GraphicalElement
con un método virtual displayOnScreen()
.
Edición 2: Esto es lo que podría parecer un método de "representación":
void render(HTMLElement root) {
for(vector<HTLMElement*>::iterator i = root.children.begin(); i != root.children.end(); i++) {
if(dynamic_cast<ActionElement*>(*i) != NULL) //Is an ActionElement
{
ActionElement* ae = dynamic_cast<ActionElement*>(*i);
ae->doAction();
render(ae);
}
else if(dynamic_cast<GraphicalElement*>(*i) != NULL) //Is a GraphicalElement
{
GraphicalElement* ge = dynamic_cast<GraphicalElement*>(*i);
ge->displayToScreen();
render(ge);
}
else
{
//Error
}
}
}
Operador dynamic_cast
resuelve el mismo problema que el envío dinámico (funciones virtuales, patrón de visitante, etc.): le permite realizar diferentes acciones según el tipo de tiempo de ejecución de un objeto.
Sin embargo, siempre debe preferir el envío dinámico, excepto quizás cuando el número de dynamic_cast
que necesita nunca crecerá.
P.ej. nunca debes hacer
if (auto v = dynamic_cast<Dog*>(animal)) { ... }
else if (auto v = dynamic_cast<Cat*>(animal)) { ... }
...
Por razones de mantenimiento y rendimiento, pero puede hacerlo, por ejemplo.
for (MenuItem* item: items)
{
if (auto submenu = dynamic_cast<Submenu*>(item))
{
auto items = submenu->items();
draw(context, items, position); // Recursion
...
}
else
{
item->draw_icon();
item->setup_accelerator();
...
}
}
que he encontrado bastante útil en esta situación exacta: tiene una sub-jerarquía muy particular que debe manejarse por separado, aquí es donde dynamic_cast
brilla. Pero los ejemplos del mundo real son bastante raros (el ejemplo del menú es algo con lo que tuve que lidiar).
Un caso de uso típico es el patrón de visitante :
struct Element
{
virtual ~Element() { }
void accept(Visitor & v)
{
v.visit(this);
}
};
struct Visitor
{
virtual void visit(Element * e) = 0;
virtual ~Visitor() { }
};
struct RedElement : Element { };
struct BlueElement : Element { };
struct FifthElement : Element { };
struct MyVisitor : Visitor
{
virtual void visit(Element * e)
{
if (RedElement * p = dynamic_cast<RedElement*>(e))
{
// do things specific to Red
}
else if (BlueElement * p = dynamic_cast<BlueElement*>(e))
{
// do things specific to Blue
}
else
{
// error: visitor doesn''t know what to do with this element
}
}
};
Ahora bien, si tienes algún Element & e;
, puedes hacer MyVisitor v;
y diga e.accept(v)
.
La característica clave del diseño es que si modifica su jerarquía de Element
, solo tiene que editar sus visitantes. El patrón todavía es bastante complejo, y solo se recomienda si tiene una jerarquía de clases muy estable de Element
s.
dynamic_cast no pretende ser una alternativa a las funciones virtuales.
dynamic_cast tiene una sobrecarga de rendimiento no trivial (o eso creo) ya que toda la jerarquía de clases tiene que ser recorrida.
dynamic_cast es similar al operador ''is'' de C # y al QueryInterface de COM viejo y bueno.
Hasta ahora he encontrado un uso real de dynamic_cast:
(*) Tiene herencia múltiple y para localizar el destino de la conversión, el compilador tiene que desplazarse por la jerarquía de clases hacia arriba y hacia abajo para ubicar el objetivo (o hacia abajo y hacia arriba si lo prefiere). Esto significa que el objetivo del reparto se encuentra en una rama paralela en relación con el origen del reparto en la jerarquía. Creo que no hay otra manera de hacer tal reparto.
En todos los demás casos, solo usa alguna clase base virtual para decirle qué tipo de objeto tiene y SOLO LUEGO usted lo envía a la clase de destino para que pueda usar parte de su funcionalidad no virtual. Idealmente, no debería haber una funcionalidad no virtual, pero qué diablos, vivimos en el mundo real.
Haciendo cosas como:
if (v = dynamic_cast(...)){} else if (v = dynamic_cast(...)){} else if ...
Es un desperdicio de rendimiento.
La programación por contrato y RTTI muestran cómo se puede usar dynamic_cast
para permitir que los objetos anuncien qué interfaces implementan. Lo usamos en mi tienda para reemplazar un sistema de metaobjetos bastante opaco. Ahora podemos describir claramente la funcionalidad de los objetos, incluso si los objetos son introducidos por un nuevo módulo varias semanas / meses después de que la plataforma fuera "horneada" (aunque, por supuesto, los contratos deben haberse decidido por adelantado).
Ejemplo de juguete
El arca de Noé funcionará como un contenedor para diferentes tipos de animales. Como el arca en sí no está preocupado por la diferencia entre monos, pingüinos y mosquitos, usted define una clase Animal
, deriva las clases Monkey
, Penguin
y Mosquito
, y almacena cada uno de ellos como un Animal
en el arca.
Una vez que termina el diluvio, Noah quiere distribuir animales a través de la tierra a los lugares donde pertenecen y, por lo tanto, necesita un conocimiento adicional sobre los animales genéricos almacenados en su arca. A modo de ejemplo, ahora puede intentar dynamic_cast<>
cada animal a un Penguin
para descubrir cuál de los animales son pingüinos que se liberarán en la Antártida y cuáles no.
Ejemplo de la vida real
Implementamos un marco de monitoreo de eventos, donde una aplicación almacenaría eventos generados en tiempo de ejecución en una lista. Los monitores de eventos pasaban por esta lista y examinaban aquellos eventos específicos en los que estaban interesados. Los tipos de eventos eran cosas a nivel del sistema operativo, como SYSCALL
, FUNCTIONCALL
e INTERRUPT
.
Aquí, almacenamos todos nuestros eventos específicos en una lista genérica de instancias de Event
. Los monitores luego iterarían sobre esta lista y dynamic_cast<>
los eventos que vieron a los tipos en los que estaban interesados. Todos los demás (aquellos que generan una excepción) se ignoran.
Pregunta : ¿Por qué no puede tener una lista separada para cada tipo de evento?
Respuesta : Puede hacer esto, pero hace que la extensión del sistema con nuevos eventos así como nuevos monitores (agregando múltiples tipos de eventos) sea más difícil, ya que todos deben conocer las listas respectivas para verificar.