c++ polymorphism rtti

c++ - ¿Por qué es preferible ''polimorfismo puro'' sobre el uso de RTTI?



polymorphism (7)

Casi todos los recursos de C ++ que he visto que analizan este tipo de cosas me dicen que debería preferir enfoques polimórficos al uso de RTTI (identificación de tipo en tiempo de ejecución). En general, tomo este tipo de consejo en serio, y trataré de entender la razón: después de todo, C ++ es una bestia poderosa y difícil de entender en toda su profundidad. Sin embargo, para esta pregunta en particular, estoy dibujando un espacio en blanco y me gustaría ver qué tipo de consejos puede ofrecer Internet. Primero, permítanme resumir lo que he aprendido hasta ahora, enumerando las razones comunes que se mencionan por qué RTTI es "considerado dañino":

Algunos compiladores no lo usan / RTTI no siempre está habilitado

Realmente no compro este argumento. Es como decir que no debería usar las funciones de C ++ 14, porque hay compiladores que no lo admiten. Y, sin embargo, nadie me desanimaría de usar las funciones de C ++ 14. La mayoría de los proyectos tendrán influencia sobre el compilador que están usando y cómo está configurado. Incluso citando la página de manual de gcc:

-fno-rtti

Inhabilite la generación de información sobre cada clase con funciones virtuales para su uso por las características de identificación de tipo en tiempo de ejecución de C ++ (dynamic_cast y typeid). Si no usa esas partes del lenguaje, puede ahorrar algo de espacio usando esta bandera. Tenga en cuenta que el manejo de excepciones utiliza la misma información, pero G ++ la genera según sea necesario. El operador dynamic_cast aún se puede usar para conversiones que no requieren información de tipo de tiempo de ejecución, es decir, conversiones a "void *" o a clases base no ambiguas.

Lo que esto me dice es que si no estoy usando RTTI, puedo desactivarlo. Es como decir, si no estás usando Boost, no tienes que vincularlo. No tengo que planificar el caso en el que alguien está compilando con -fno-rtti . Además, el compilador fallará alto y claro en este caso.

Cuesta memoria extra / puede ser lento

Cada vez que tengo la tentación de usar RTTI, eso significa que necesito acceder a algún tipo de información de tipo o rasgo de mi clase. Si implemento una solución que no usa RTTI, esto generalmente significa que tendré que agregar algunos campos a mis clases para almacenar esta información, por lo que el argumento de la memoria es un poco nulo (daré un ejemplo de esto más adelante).

Un dynamic_cast puede ser lento, de hecho. Sin embargo, generalmente hay formas de evitar tener que usarlo en situaciones críticas para la velocidad. Y no veo la alternativa. Esta respuesta SO sugiere utilizar una enumeración, definida en la clase base, para almacenar el tipo. Eso solo funciona si conoce todas sus clases derivadas a priori. Eso es un gran "si"!

De esa respuesta, también parece que el costo de RTTI tampoco está claro. Diferentes personas miden cosas diferentes.

Elegantes diseños polimórficos harán que RTTI sea innecesario

Este es el tipo de consejo que me tomo en serio. En este caso, simplemente no puedo encontrar buenas soluciones que no sean RTTI que cubran mi caso de uso RTTI. Déjame darte un ejemplo:

Digamos que estoy escribiendo una biblioteca para manejar gráficos de algún tipo de objetos. Quiero permitir que los usuarios generen sus propios tipos cuando utilicen mi biblioteca (por lo que el método enum no está disponible). Tengo una clase base para mi nodo:

class node_base { public: node_base(); virtual ~node_base(); std::vector< std::shared_ptr<node_base> > get_adjacent_nodes(); };

Ahora, mis nodos pueden ser de diferentes tipos. Que tal esto:

class red_node : virtual public node_base { public: red_node(); virtual ~red_node(); void get_redness(); }; class yellow_node : virtual public node_base { public: yellow_node(); virtual ~yellow_node(); void set_yellowness(int); };

Demonios, ¿por qué ni siquiera uno de estos?

class orange_node : public red_node, public yellow_node { public: orange_node(); virtual ~orange_node(); void poke(); void poke_adjacent_oranges(); };

La última función es interesante. Aquí hay una manera de escribirlo:

void orange_node::poke_adjacent_oranges() { auto adj_nodes = get_adjacent_nodes(); foreach(auto node, adj_nodes) { // In this case, typeid() and static_cast might be faster std::shared_ptr<orange_node> o_node = dynamic_cast<orange_node>(node); if (o_node) { o_node->poke(); } } }

Todo esto parece claro y limpio. No tengo que definir atributos o métodos donde no los necesito, la clase de nodo base puede mantenerse delgada y media. Sin RTTI, ¿por dónde empiezo? Tal vez pueda agregar un atributo node_type a la clase base:

class node_base { public: node_base(); virtual ~node_base(); std::vector< std::shared_ptr<node_base> > get_adjacent_nodes(); private: std::string my_type; };

¿Es std :: string una buena idea para un tipo? Quizás no, pero ¿qué más puedo usar? Inventar un número y esperar que nadie más lo esté usando todavía? Además, en el caso de mi orange_node, ¿qué pasa si quiero usar los métodos de red_node y yellow_node? ¿Tendría que almacenar varios tipos por nodo? Eso parece complicado.

Conclusión

Estos ejemplos no parecen demasiado complejos o inusuales (estoy trabajando en algo similar en mi trabajo diario, donde los nodos representan hardware real que se controla a través del software y que hacen cosas muy diferentes según lo que sean). Sin embargo, no conocería una forma limpia de hacerlo con plantillas u otros métodos. Tenga en cuenta que estoy tratando de entender el problema, no defender mi ejemplo. Mi lectura de páginas como la respuesta SO que vinculé anteriormente y esta página en Wikilibros parece sugerir que estoy usando indebidamente RTTI, pero me gustaría saber por qué.

Entonces, volviendo a mi pregunta original: ¿Por qué es preferible ''polimorfismo puro'' sobre el uso de RTTI?


Algunos compiladores no lo usan / RTTI no siempre está habilitado

Creo que has entendido mal tales argumentos.

Hay varios lugares de codificación de C ++ en los que no se debe utilizar RTTI. Donde los conmutadores del compilador se usan para deshabilitar por la fuerza RTTI. Si está codificando dentro de tal paradigma ... entonces seguramente ya ha sido informado de esta restricción.

El problema, por lo tanto, es con las bibliotecas . Es decir, si está escribiendo una biblioteca que depende de RTTI, los usuarios que desactivan RTTI no pueden usar su biblioteca. Si desea que su biblioteca sea utilizada por esas personas, entonces no puede usar RTTI, incluso si su biblioteca también es utilizada por personas que pueden usar RTTI. Igualmente importante, si no puede usar RTTI, tiene que darse una vuelta por las bibliotecas, ya que el uso de RTTI es un factor decisivo para usted.

Cuesta memoria extra / puede ser lento

Hay muchas cosas que no haces en hot loops. No asignas memoria. No vas iterando a través de listas vinculadas. Etcétera. RTTI ciertamente puede ser otra de esas cosas de "no hagas esto aquí".

Sin embargo, considere todos sus ejemplos de RTTI. En todos los casos, tiene uno o más objetos de tipo indeterminado y desea realizar alguna operación en ellos que puede no ser posible para algunos de ellos.

Eso es algo que tiene que solucionar a nivel de diseño . Puede escribir contenedores que no asignen memoria que se ajuste al paradigma "STL". Puede evitar las estructuras de datos de la lista vinculada o limitar su uso. Puede reorganizar matrices de estructuras en estructuras de matrices o lo que sea. Cambia algunas cosas, pero puedes mantenerlo compartimentado.

¿Cambiar una operación RTTI compleja en una llamada de función virtual normal? Ese es un problema de diseño. Si tiene que cambiar eso, entonces es algo que requiere cambios en cada clase derivada. Cambia la forma en que gran cantidad de código interactúa con varias clases. El alcance de dicho cambio se extiende mucho más allá de las secciones de código críticas para el rendimiento.

Entonces ... ¿por qué lo escribiste de la manera incorrecta para empezar?

No tengo que definir atributos o métodos donde no los necesito, la clase de nodo base puede mantenerse delgada y media.

¿A que final?

Dices que la clase base es "delgada y mala". Pero en realidad ... no existe . En realidad no hace nada .

Solo mira tu ejemplo: node_base . ¿Qué es? Parece ser una cosa que tiene otras cosas adyacentes. Esta es una interfaz Java (Java pre-genérica): una clase que existe únicamente para ser algo que los usuarios pueden transmitir al tipo real . Tal vez agregue alguna característica básica como adyacencia (Java agrega ToString ), pero eso es todo.

Hay una diferencia entre "delgado y medio" y "transparente".

Como dijo Yakk, tales estilos de programación se limitan a sí mismos en la interoperabilidad, porque si toda la funcionalidad está en una clase derivada, los usuarios fuera de ese sistema, sin acceso a esa clase derivada, no pueden interactuar con el sistema. No pueden anular las funciones virtuales y agregar nuevos comportamientos. Ni siquiera pueden llamar a esas funciones.

Pero lo que también hacen es hacer un gran dolor hacer cosas nuevas, incluso dentro del sistema. Considere su función poke_adjacent_oranges . ¿Qué sucede si alguien quiere un tipo lime_node que se pueda orange_node como orange_node s? Bueno, no podemos derivar lime_node de orange_node ; eso no tiene sentido.

En cambio, tenemos que agregar un nuevo lime_node derivado de node_base . Luego cambie el nombre de poke_adjacent_oranges a poke_adjacent_pokables . Y luego, intente orange_node a orange_node y lime_node ; el reparto que funcione es el que empujamos.

Sin embargo, lime_node necesita su propio poke_adjacent_pokables . Y esta función necesita hacer las mismas verificaciones de lanzamiento.

Y si agregamos un tercer tipo, no solo tenemos que agregar su propia función, sino que debemos cambiar las funciones en las otras dos clases.

Obviamente, ahora haces poke_adjacent_pokables una función libre, para que funcione para todos ellos. Pero, ¿qué supone que sucede si alguien agrega un cuarto tipo y se olvida de agregarlo a esa función?

Hola, rotura silenciosa . El programa parece funcionar más o menos bien, pero no lo es. Si poke hubiera sido una función virtual real , el compilador habría fallado cuando no hubiera node_base la función virtual pura de node_base .

A su manera, no tiene tales comprobaciones de compilación. Ah, claro, el compilador no buscará virtuales no puros, pero al menos tiene protección en los casos en que la protección es posible (es decir, no hay una operación predeterminada).

El uso de clases base transparentes con RTTI conduce a una pesadilla de mantenimiento. De hecho, la mayoría de los usos de RTTI conduce a dolores de cabeza por mantenimiento. Eso no significa que RTTI no sea útil (es vital para hacer boost::any trabajo, por ejemplo). Pero es una herramienta muy especializada para necesidades muy especializadas.

De esa manera, es "dañino" de la misma manera que goto . Es una herramienta útil que no debe eliminarse. Pero su uso debería ser raro dentro de su código.

Entonces, si no puede usar clases base transparentes y conversión dinámica, ¿cómo evita las interfaces gordas? ¿Cómo evitas burbujear cada función que quieras llamar en un tipo de burbujear hasta la clase base?

La respuesta depende de para qué es la clase base.

Las clases base transparentes como node_base simplemente están usando la herramienta incorrecta para el problema. Las listas vinculadas se manejan mejor con plantillas. El tipo de nodo y la adyacencia serían proporcionados por un tipo de plantilla. Si desea poner un tipo polimórfico en la lista, puede hacerlo. Simplemente use BaseClass* como T en el argumento de la plantilla. O tu puntero inteligente preferido.

Pero hay otros escenarios. Uno es un tipo que hace muchas cosas, pero tiene algunas partes opcionales. Una instancia particular podría implementar ciertas funciones, mientras que otra no. Sin embargo, el diseño de tales tipos generalmente ofrece una respuesta adecuada.

La clase "entidad" es un ejemplo perfecto de esto. Esta clase ha afectado a los desarrolladores de juegos desde hace mucho tiempo. Conceptualmente, tiene una interfaz gigantesca, que vive en la intersección de casi una docena de sistemas completamente dispares. Y diferentes entidades tienen diferentes propiedades. Algunas entidades no tienen ninguna representación visual, por lo que sus funciones de representación no hacen nada. Y todo esto está determinado en tiempo de ejecución.

La solución moderna para esto es un sistema de estilo componente. Entity es simplemente un contenedor de un conjunto de componentes, con algo de pegamento entre ellos. Algunos componentes son opcionales; una entidad que no tiene representación visual no tiene el componente "gráficos". Una entidad sin IA no tiene un componente "controlador". Etcétera.

Las entidades en dicho sistema son solo punteros a los componentes, y la mayoría de su interfaz se proporciona accediendo a los componentes directamente.

El desarrollo de un sistema de componentes de este tipo requiere reconocer, en la etapa de diseño, que ciertas funciones se agrupan conceptualmente, de modo que todos los tipos que implementan una las implementarán todas. Esto le permite extraer la clase de la clase base prospectiva y convertirla en un componente separado.

Esto también ayuda a seguir el Principio de responsabilidad única. Dicha clase compuesta solo tiene la responsabilidad de ser titular de componentes.

De Matthew Walton:

Noto que muchas respuestas no tienen en cuenta la idea de que su ejemplo sugiere que node_base es parte de una biblioteca y los usuarios crearán sus propios tipos de nodos. Entonces no pueden modificar node_base para permitir otra solución, por lo que tal vez RTTI se convierta en su mejor opción.

OK, exploremos eso.

Para que esto tenga sentido, lo que tendría que tener es una situación en la que alguna biblioteca L proporciona un contenedor u otro soporte de datos estructurado. El usuario puede agregar datos a este contenedor, iterar sobre su contenido, etc. Sin embargo, la biblioteca realmente no hace nada con estos datos; simplemente gestiona su existencia.

Pero ni siquiera maneja su existencia tanto como su destrucción . La razón es que, si se espera que use RTTI para tales fines, entonces está creando clases que L ignora. Esto significa que su código asigna el objeto y se lo entrega a L para su administración.

Ahora, hay casos en los que algo como esto es un diseño legítimo. Señalización de eventos / paso de mensajes, colas de trabajo seguras para subprocesos, etc. El patrón general aquí es el siguiente: alguien está realizando un servicio entre dos partes de código que es apropiado para cualquier tipo, pero el servicio no necesita conocer los tipos específicos involucrados .

En C, este patrón se deletrea void* , y su uso requiere mucho cuidado para evitar que se rompa. En C ++, este patrón se deletrea std::experimental::any (pronto se deletreará std::any ).

La forma en que esto debería funcionar es que L proporciona una clase node_base que toma any que represente sus datos reales. Cuando recibe el mensaje, el elemento de trabajo de la cola de subprocesos, o lo que sea que esté haciendo, lo convierte en su tipo apropiado, que tanto el remitente como el receptor conocen.

Entonces, en lugar de derivar orange_node de node_data , simplemente pegue una orange dentro de any campo miembro de node_data . El usuario final lo extrae y usa any_cast para convertirlo a orange . Si el yeso falla, entonces no era orange .

Ahora, si está familiarizado con la implementación de any , es probable que diga: "oye, espere un minuto: any interno utiliza RTTI para hacer que any_cast funcione". A lo que respondo, "... sí".

Ese es el punto de una abstracción . En el fondo de los detalles, alguien está usando RTTI. Pero en el nivel en el que debería estar operando, el RTTI directo no es algo que deba hacer.

Debería utilizar tipos que le brinden la funcionalidad que desea. Después de todo, realmente no quieres RTTI. Lo que desea es una estructura de datos que pueda almacenar un valor de un tipo dado, ocultarlo a todos excepto al destino deseado, y luego volver a convertirlo en ese tipo, con la verificación de que el valor almacenado en realidad es de ese tipo.

Eso se llama any . Utiliza RTTI, pero usar any es muy superior al uso de RTTI directamente, ya que se ajusta a la semántica deseada más correctamente.


C ++ se basa en la idea de la verificación de tipos estáticos.

[1] RTTI, es decir, dynamic_cast y type_id , es la comprobación dinámica de tipos.

Entonces, esencialmente se pregunta por qué la verificación de tipo estática es preferible a la verificación de tipo dinámica. Y la respuesta simple es, si la verificación de tipo estática es preferible a la verificación de tipo dinámica, depende . En mucho Pero C ++ es uno de los lenguajes de programación diseñados en torno a la idea de la verificación de tipos estáticos. Y esto significa que, por ejemplo, el proceso de desarrollo, en particular las pruebas, generalmente se adapta a la verificación de tipo estático y luego se ajusta mejor.

Re

" No sabría una manera limpia de hacer esto con plantillas u otros métodos

puede hacer este proceso-nodos-heterogéneos-de-un-gráfico con comprobación de tipo estático y sin conversión de ningún tipo a través del patrón de visitante, por ejemplo, así:

#include <iostream> #include <set> #include <initializer_list> namespace graph { using std::set; class Red_thing; class Yellow_thing; class Orange_thing; struct Callback { virtual void handle( Red_thing& ) {} virtual void handle( Yellow_thing& ) {} virtual void handle( Orange_thing& ) {} }; class Node { private: set<Node*> connected_; public: virtual void call( Callback& cb ) = 0; void connect_to( Node* p_other ) { connected_.insert( p_other ); } void call_on_connected( Callback& cb ) { for( auto const p : connected_ ) { p->call( cb ); } } virtual ~Node(){} }; class Red_thing : public virtual Node { public: void call( Callback& cb ) override { cb.handle( *this ); } auto redness() -> int { return 255; } }; class Yellow_thing : public virtual Node { public: void call( Callback& cb ) override { cb.handle( *this ); } }; class Orange_thing : public Red_thing , public Yellow_thing { public: void call( Callback& cb ) override { cb.handle( *this ); } void poke() { std::cout << "Poked!/n"; } void poke_connected_orange_things() { struct Poker: Callback { void handle( Orange_thing& obj ) override { obj.poke(); } } poker; call_on_connected( poker ); } }; } // namespace graph auto main() -> int { using namespace graph; Red_thing r; Yellow_thing y1, y2; Orange_thing o1, o2, o3; for( Node* p : std::initializer_list<Node*>{ &y1, &y2, &r, &o2, &o3 } ) { o1.connect_to( p ); } o1.poke_connected_orange_things(); }

Esto supone que se conoce el conjunto de tipos de nodo.

Cuando no lo es, el patrón de visitante (hay muchas variaciones) puede expresarse con unos pocos moldes centralizados o solo uno.

Para un enfoque basado en plantillas, consulte la biblioteca Boost Graph. Es triste decir que no estoy familiarizado con él, no lo he usado. Por lo tanto, no estoy seguro de qué hace exactamente y cómo, y en qué medida usa la verificación de tipo estático en lugar de RTTI, pero dado que Boost generalmente se basa en plantillas con la verificación de tipo estático como idea central, creo que encontrará que su sub-biblioteca Graph también se basa en la verificación de tipos estáticos.

[1] Información del tipo de tiempo de ejecución .


Por supuesto, hay un escenario donde el polimorfismo no puede ayudar: los nombres. typeid le permite acceder al nombre del tipo, aunque la forma en que se codifica este nombre está definida por la implementación. Pero generalmente esto no es un problema ya que puede comparar dos typeid -s:

if ( typeid(5) == "int" ) // may be false if ( typeid(5) == typeid(int) ) // always true

Lo mismo vale para los hashes.

[...] RTTI es "considerado dañino"

dañina es, sin duda una exageración: RTTI tiene algunos inconvenientes, pero lo hace tener ventajas también.

Realmente no tienes que usar RTTI. RTTI es una herramienta para resolver problemas de OOP: si usa otro paradigma, estos probablemente desaparecerían. C no tiene RTTI, pero aún funciona. En cambio, C ++ es totalmente compatible con OOP y le brinda múltiples herramientas para superar algunos problemas que pueden requerir información de tiempo de ejecución: una de ellas es RTTI, que a pesar de que tiene un precio. Si no puede permitírselo, lo que es mejor decir solo después de un análisis de rendimiento seguro, sigue existiendo la vieja escuela void* : es gratis. Gratuito. Pero no obtienes ningún tipo de seguridad. Entonces se trata de intercambios.

  • Algunos compiladores no usan / RTTI no siempre está habilitado
    Realmente no compro este argumento. Es como decir que no debería usar las funciones de C ++ 14, porque hay compiladores que no lo admiten. Y, sin embargo, nadie me desanimaría de usar las funciones de C ++ 14.

Si escribe (posiblemente estrictamente) un código C ++ conforme, puede esperar el mismo comportamiento independientemente de la implementación. Las implementaciones que cumplen con los estándares deberán admitir características estándar de C ++.

Pero tenga en cuenta que, en algunos entornos, C ++ define ("independientes"), no es necesario proporcionar RTTI y tampoco las excepciones, virtual y así sucesivamente. RTTI necesita una capa subyacente para funcionar correctamente que se ocupe de detalles de bajo nivel como el ABI y la información de tipo real.

Estoy de acuerdo con Yakk con respecto a RTTI en este caso. Sí, podría ser usado; pero es lógicamente correcto? El hecho de que el idioma le permita omitir esta verificación no significa que deba hacerse.


La mayor parte de la persuasión moral contra este o aquel rasgo es la tipicidad originada por la observación de que hay un número de usos erróneos de ese rasgo.

Donde los moralistas fallan es que suponen que TODOS los usos están mal concebidos, mientras que, de hecho, las características existen por una razón.

Tienen lo que solía llamar el "complejo de plomería": piensan que todos los grifos funcionan mal porque todos los grifos que están llamados a reparar son. La realidad es que la mayoría de los grifos funcionan bien: ¡simplemente no se debe llamar a un plomero por ellos!

Una cosa loca que puede suceder es cuando, para evitar el uso de una característica dada, los programadores escriben una gran cantidad de código repetitivo que en realidad re-implementa de manera privada exactamente esa característica. (¿Alguna vez ha conocido clases que no usan RTTI ni llamadas virtuales, pero tienen un valor para rastrear qué tipo derivado real son? Eso no es más que la reinvención de RTTI disfrazada).

Hay una forma general de pensar sobre el polimorfismo: IF(selection) CALL(something) WITH(parameters) . (Lo siento, pero la programación, al ignorar la abstracción, se trata de eso)

El uso de tiempo de diseño (conceptos) en tiempo de compilación (basado en la deducción de plantillas), tiempo de ejecución (herencia y función virtual basada) o polimorfismo basado en datos (RTTI y conmutación), depende de la cantidad de decisiones conocidas. en cada una de las etapas de la producción y qué tan variables son en cada contexto.

La idea es que:

cuanto más pueda anticipar, mayor será la posibilidad de detectar errores y evitar errores que afecten al usuario final.

Si todo es constante (incluidos los datos), puede hacer todo con metaprogramación de plantilla. Después de que se realizó la compilación en las constantes actualizadas, todo el programa se reduce a solo una declaración de retorno que escupe el resultado .

Si hay varios casos que se conocen en el momento de la compilación , pero no conoce los datos reales sobre los que tienen que actuar, entonces el polimorfismo en tiempo de compilación (principalmente CRTP o similar) puede ser una solución.

Si la selección de los casos depende de los datos (valores no conocidos en tiempo de compilación) y la conmutación es unidimensional (qué hacer puede reducirse a un solo valor), entonces el despacho basado en funciones virtuales (o en general "tablas de punteros de función" ") es necesario.

Si el cambio es multidimensional, dado que no existe un despacho de tiempo de ejecución múltiple nativo en C ++, entonces debe:

  • Reduzca a una dimensión mediante Goedelization : ahí es donde se encuentran las bases virtuales y la herencia múltiple, con diamantes y paralelogramos apilados , pero esto requiere que el número de combinación posible sea conocido y sea relativamente pequeño.
  • Encadene las dimensiones una dentro de la otra (como en el patrón de visitantes compuestos, pero esto requiere que todas las clases sean conscientes de sus otros hermanos, por lo que no puede "escalar" desde el lugar en que fue concebido)
  • Despachar llamadas basadas en múltiples valores. Para eso es exactamente RTTI.

Si no es solo el cambio, sino que incluso las acciones no se conocen en tiempo de compilación, entonces se requieren secuencias de comandos y análisis : los datos en sí mismos deben describir la acción que se tomará en ellos.

Ahora, dado que cada uno de los casos que enumeré puede verse como un caso particular de lo que sigue, puede resolver todos los problemas al abusar de la solución más baja también para problemas asequibles con la más alta.

Eso es lo que la moralización realmente empuja a evitar. ¡Pero eso no significa que no existan problemas en los dominios más bajos!

Bashing RTTI solo para golpearlo, es como golpear goto solo para golpearlo. Cosas para loros, no para programadores.


Parece un poco ordenado en un pequeño ejemplo, pero en la vida real pronto terminarás con un largo conjunto de tipos que pueden empujarse entre sí, algunos de ellos quizás solo en una dirección.

¿Qué pasa con dark_orange_node , o black_and_orange_striped_node , o dotted_node ? ¿Puede tener puntos de diferentes colores? ¿Qué pasa si la mayoría de los puntos son de color naranja, se puede pinchar entonces?

Y cada vez que tenga que agregar una nueva regla, tendrá que volver a visitar todas las funciones poke_adjacent y agregar más sentencias if.

Como siempre, es difícil crear ejemplos genéricos, te lo daré.

Pero si tuviera que hacer este ejemplo específico , agregaría un miembro poke() a todas las clases y dejaría que algunos ignoren la llamada ( void poke() {} ) si no están interesados.

Seguramente eso sería incluso menos costoso que comparar los typeid s.


Si llama a una función, por regla general, no le importa qué pasos precisos tomará, solo que se logrará un objetivo de nivel superior dentro de ciertas restricciones (y cómo la función hace que eso suceda es realmente un problema propio).

Cuando usa RTTI para realizar una preselección de objetos especiales que pueden hacer un determinado trabajo, mientras que otros en el mismo conjunto no pueden, está rompiendo esa visión cómoda del mundo. De repente, se supone que la persona que llama sabe quién puede hacer qué, en lugar de simplemente decirle a sus secuaces que continúen. Algunas personas están molestas por esto, y sospecho que esta es una gran parte de la razón por la cual RTTI se considera un poco sucio.

¿Hay algún problema de rendimiento? Tal vez, pero nunca lo he experimentado, y podría ser sabiduría de hace veinte años, o de personas que honestamente creen que usar tres instrucciones de montaje en lugar de dos es una hinchazón inaceptable.

Entonces, cómo tratarlo ... Dependiendo de su situación, puede tener sentido tener propiedades específicas de nodo agrupadas en objetos separados (es decir, toda la API ''naranja'' podría ser un objeto separado). El objeto raíz podría tener una función virtual para devolver la API ''naranja'', devolviendo nullptr por defecto para los objetos que no son de color naranja.

Si bien esto puede ser excesivo dependiendo de su situación, le permitiría consultar a nivel raíz si un nodo específico admite una API específica y, si lo hace, ejecutar funciones específicas para esa API.


Una interfaz describe lo que uno necesita saber para interactuar en una situación dada en el código. Una vez que extiende la interfaz con "toda su jerarquía de tipos", la "superficie" de su interfaz se vuelve enorme, lo que dificulta el razonamiento al respecto.

Como ejemplo, su "empuje de naranjas adyacentes" significa que yo, como tercero, ¡no puedo emular ser una naranja! Usted declaró en privado un tipo naranja, luego use RTTI para hacer que su código se comporte especial al interactuar con ese tipo. Si quiero "ser naranja", debo estar dentro de su jardín privado.

Ahora, todos los que se acoplan con "orangeness" se juntan con todo su tipo naranja, e implícitamente con todo su jardín privado, en lugar de con una interfaz definida.

Si bien a primera vista parece una excelente manera de extender la interfaz limitada sin tener que cambiar todos los clientes (agregando am_I_orange ), lo que suele suceder es que osifica la base del código y evita una mayor extensión. La orangenia especial se vuelve inherente al funcionamiento del sistema, y ​​le impide crear un reemplazo de "mandarina" para la naranja que se implementa de manera diferente y tal vez elimina una dependencia o resuelve algún otro problema con elegancia.

Esto significa que su interfaz tiene que ser suficiente para resolver su problema. Desde esa perspectiva, ¿por qué solo necesita pinchar naranjas, y si es así, por qué no estaba disponible en la interfaz? Si necesita un conjunto difuso de etiquetas que se pueden agregar ad-hoc, puede agregarlo a su tipo:

class node_base { public: bool has_tag(tag_name);

Esto proporciona una ampliación masiva similar de su interfaz desde una especificación estrecha a una amplia basada en etiquetas. Excepto en lugar de hacerlo a través de RTTI y detalles de implementación (también conocido como "¿cómo se implementa? ¿Con el tipo naranja? Ok, apruebas"), lo hace con algo fácilmente emulado a través de una implementación completamente diferente.

Esto incluso puede extenderse a métodos dinámicos, si lo necesita. "¿Apoya que te engañen con argumentos Baz, Tom y Alice? Ok, engañarte". En un gran sentido, esto es menos intrusivo que un reparto dinámico para llegar al hecho de que el otro objeto es un tipo que usted conoce.

Ahora los objetos de mandarina pueden tener la etiqueta naranja y reproducirse, mientras se desacoplan la implementación.

Todavía puede conducir a un gran desastre, pero es al menos un desastre de mensajes y datos, no jerarquías de implementación.

La abstracción es un juego de desacoplamiento y ocultación de irrelevancias. Hace que el código sea más fácil de razonar localmente. RTTI está abriendo un agujero directamente a través de la abstracción en los detalles de implementación. Esto puede facilitar la resolución de un problema, pero tiene el costo de encerrarlo en una implementación específica con mucha facilidad.