sencillos poo polimorfismo parametrico herencia estatico ejemplos dinamico c++ paradigms

poo - Técnicas de C++: borrado de tipo vs. polimorfismo puro



polimorfismo parametrico (3)

Aquí hay una vista: la pregunta parece preguntarse cómo se debe elegir entre la vinculación tardía ("polimorfismo en tiempo de ejecución") y la vinculación temprana ("polimorfismo en tiempo de compilación").

Como KerrekSB señala en sus comentarios, hay algunas cosas que puede hacer con la vinculación tardía que simplemente no es realista hacer con la vinculación temprana. Muchos usos del patrón de estrategia (descodificación de E / S de red) o el patrón de fábrica abstracta (fábricas de clase seleccionadas por el tiempo de ejecución) entran en esta categoría.

Si ambos enfoques son viables, entonces la elección es una cuestión de los compromisos involucrados. En las aplicaciones de C ++, las principales compensaciones que veo entre la vinculación temprana y la tardía son la capacidad de mantenimiento de la implementación, el tamaño binario y el rendimiento.

Hay al menos algunas personas que sienten que las plantillas de C ++ en cualquier forma o forma son imposibles de comprender. O posiblemente tenga alguna otra reserva menos dramática con plantillas. Las plantillas de C ++ tienen muchos pequeños errores ("¿cuándo necesito usar las palabras clave ''typename'' y ''template''?"), Y trucos no obvios (SFINAE viene a la mente).

Otra compensación es la optimización. Cuando se vincula temprano, le da al compilador más información sobre su programa, y ​​así puede (potencialmente) hacer un mejor trabajo optimizando. Cuando se vincula tarde, el compilador (probablemente) no sabe de antemano tanta información; parte de esa información puede estar en otras unidades de compilación, por lo que el optimizador no puede hacer tanto.

Otra compensación es el tamaño del programa. Al menos en C ++, el uso de "polimorfismo en tiempo de compilación" a veces aumenta el tamaño binario, ya que el compilador crea, optimiza y emite códigos diferentes para cada especialización utilizada. En contraste, cuando se vincula tarde, solo hay una ruta de código.

Es interesante comparar la misma compensación que se está haciendo en un contexto diferente. Tome aplicaciones web, donde se utiliza (algún tipo de) polimorfismo para hacer frente a las diferencias entre los navegadores, y posiblemente para la internacionalización (i18n) / localización. Ahora, una aplicación web JavaScript escrita a mano probablemente usaría lo que equivale a un enlace tardío aquí, al tener métodos que detectan capacidades en tiempo de ejecución para determinar qué hacer. Las bibliotecas como jQuery toman esta dirección.

Otro enfoque es escribir un código diferente para cada posible navegador / i18n posibilidad. Si bien esto suena absurdo, está lejos de ser desconocido. El Google Web Toolkit utiliza este enfoque. GWT tiene su mecanismo de "enlace diferido", que se utiliza para especializar la salida del compilador a diferentes navegadores y diferentes localizaciones. El mecanismo de "enlace diferido" de GWT usa un enlace temprano: el compilador GWT de Java a JavaScript descubre todas las formas posibles en que el polimorfismo podría ser necesario, y escupe un "binario" completamente diferente para cada uno.

Las compensaciones son similares. Envolver su cabeza en torno a la forma en que extiende GWT mediante un enlace diferido puede ser un dolor de cabeza y medio; Tener conocimientos en el momento de la compilación permite al compilador de GWT optimizar cada especialización por separado, posiblemente obteniendo un mejor rendimiento y un tamaño más pequeño para cada especialización; La totalidad de una aplicación GWT puede llegar a ser varias veces el tamaño de una aplicación jQuery comparable, debido a todas las especializaciones precompiladas.

¿Cuáles son las ventajas / desventajas de las dos técnicas en comparación? Y, lo que es más importante, ¿por qué y cuándo debería usarse uno sobre el otro? ¿Es solo una cuestión de gusto / preferencia personal?

A lo mejor de mis habilidades, no he encontrado otra publicación que aborde explícitamente mi pregunta. Entre las muchas preguntas sobre el uso real del polimorfismo y / o el borrado de tipo, las siguientes parecen ser las más cercanas, o al menos eso parece, pero en realidad tampoco aborda mi pregunta:

C ++ - y CRTP. Borrado de tipo vs polimorfismo

Por favor, tenga en cuenta que entiendo muy bien ambas técnicas. Para este fin, proporciono un ejemplo simple, autónomo y funcional a continuación, que me complace eliminar, si se considera innecesario. Sin embargo, el ejemplo debería aclarar qué significan las dos técnicas con respecto a mi pregunta. No me interesa discutir las nomenclaturas. Además, sé la diferencia entre el polimorfismo en tiempo de compilación y de ejecución, aunque no consideraría que esto sea relevante para la pregunta. Tenga en cuenta que mi interés es menor en las diferencias de rendimiento, si las hay. Sin embargo, si hubiera un argumento llamativo para uno u otro basado en el rendimiento, tendría curiosidad por leerlo. En particular, me gustaría conocer ejemplos concretos (sin código) que realmente solo funcionen con uno de los dos enfoques.

Al observar el ejemplo a continuación, una diferencia principal es la administración de la memoria, que para el polimorfismo permanece en el lado del usuario, y para el borrado de tipo se guarda cuidadosamente y se requiere cierto recuento de referencias (o aumento). Dicho esto, dependiendo de los escenarios de uso, la situación podría mejorarse para el ejemplo de polimorfismo mediante el uso de indicadores inteligentes con el vector (?), Aunque para casos arbitrarios esto puede resultar poco práctico (?). Otro aspecto, potencialmente a favor del borrado de tipo, puede ser la independencia de una interfaz común, pero ¿por qué exactamente eso sería una ventaja (?).

El código que se proporciona a continuación se probó (compiló y ejecutó) con MS VisualStudio 2008 simplemente colocando todos los siguientes bloques de código en un único archivo fuente. También debería compilar con gcc en Linux, o eso espero / supongo, porque no veo ninguna razón por la que no (?) :-) He dividido / dividido el código aquí para mayor claridad.

Estos archivos de encabezado deberían ser suficientes, ¿verdad (?).

#include <iostream> #include <vector> #include <string>

Recuento de referencia simple para evitar dependencias de impulso (u otras). Esta clase solo se usa en el siguiente ejemplo de borrado de tipo.

class RefCount { RefCount( const RefCount& ); RefCount& operator= ( const RefCount& ); int m_refCount; public: RefCount() : m_refCount(1) {} void Increment() { ++m_refCount; } int Decrement() { return --m_refCount; } };

Este es el ejemplo / ilustración simple de borrado de tipo. Fue copiado y modificado en parte del siguiente artículo. Principalmente he tratado de hacerlo lo más claro y directo posible. http://www.cplusplus.com/articles/oz18T05o/

class Object { struct ObjectInterface { virtual ~ObjectInterface() {} virtual std::string GetSomeText() const = 0; }; template< typename T > struct ObjectModel : ObjectInterface { ObjectModel( const T& t ) : m_object( t ) {} virtual ~ObjectModel() {} virtual std::string GetSomeText() const { return m_object.GetSomeText(); } T m_object; }; void DecrementRefCount() { if( mp_refCount->Decrement()==0 ) { delete mp_refCount; delete mp_objectInterface; mp_refCount = NULL; mp_objectInterface = NULL; } } Object& operator= ( const Object& ); ObjectInterface *mp_objectInterface; RefCount *mp_refCount; public: template< typename T > Object( const T& obj ) : mp_objectInterface( new ObjectModel<T>( obj ) ), mp_refCount( new RefCount ) {} ~Object() { DecrementRefCount(); } std::string GetSomeText() const { return mp_objectInterface->GetSomeText(); } Object( const Object &obj ) { obj.mp_refCount->Increment(); mp_refCount = obj.mp_refCount; mp_objectInterface = obj.mp_objectInterface; } }; struct MyObject1 { std::string GetSomeText() const { return "MyObject1"; } }; struct MyObject2 { std::string GetSomeText() const { return "MyObject2"; } }; void UseTypeErasure() { typedef std::vector<Object> ObjVect; typedef ObjVect::const_iterator ObjVectIter; ObjVect objVect; objVect.push_back( Object( MyObject1() ) ); objVect.push_back( Object( MyObject2() ) ); for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter ) std::cout << iter->GetSomeText(); }

En lo que a mí respecta, esto parece lograr más o menos lo mismo usando polimorfismo, o tal vez no (?).

struct ObjectInterface { virtual ~ObjectInterface() {} virtual std::string GetSomeText() const = 0; }; struct MyObject3 : public ObjectInterface { std::string GetSomeText() const { return "MyObject3"; } }; struct MyObject4 : public ObjectInterface { std::string GetSomeText() const { return "MyObject4"; } }; void UsePolymorphism() { typedef std::vector<ObjectInterface*> ObjVect; typedef ObjVect::const_iterator ObjVectIter; ObjVect objVect; objVect.push_back( new MyObject3 ); objVect.push_back( new MyObject4 ); for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter ) std::cout << (*iter)->GetSomeText(); for( ObjVectIter iter = objVect.begin(); iter != objVect.end(); ++iter ) delete *iter; }

Y finalmente para probar todo lo anterior juntos.

int main() { UseTypeErasure(); UsePolymorphism(); return(0); }


Polimorfismo basado en el método virtual de estilo C ++:

  1. Tienes que usar clases para mantener tus datos.
  2. Cada clase tiene que construirse teniendo en cuenta su tipo particular de polimorfismo.
  3. Cada clase tiene una dependencia común de nivel binario, que restringe cómo el compilador crea la instancia de cada clase.
  4. Los datos que está abstrayendo deben describir explícitamente una interfaz que describa sus necesidades.

Borrado de tipo basado en plantillas de estilo C ++ (con polimorfismo basado en método virtual que realiza el borrado):

  1. Tienes que usar plantilla para hablar de tus datos.
  2. Cada parte de los datos en los que está trabajando puede no estar relacionada con otras opciones.
  3. El trabajo de borrado de tipo se realiza dentro de los archivos de encabezado públicos, que aumentan el tiempo de compilación.
  4. Cada tipo borrado tiene su propia plantilla instanciada, que puede aumentar el tamaño binario.
  5. Los datos que está extrayendo no necesitan escribirse como dependientes directos de sus necesidades.

Ahora, ¿cuál es mejor? Bueno, eso depende de si las cosas anteriores son buenas o malas en su situación particular.

Como ejemplo explícito, std::function<...> utiliza el borrado de tipo que le permite tomar punteros de función, referencias de función, salida de una pila completa de funciones basadas en plantillas que generan tipos en tiempo de compilación, myraids de functors que tienen Un operador (), y lambdas. Todos estos tipos no están relacionados entre sí. Y debido a que no están vinculados a tener un virtual operator() , cuando se usan fuera del contexto std::function la abstracción que representan puede compilarse. No podría hacer esto sin borrar el tipo, y probablemente no querría hacerlo.

Por otro lado, solo porque una clase tenga un método llamado DoFoo , no significa que todos hagan lo mismo. Con el polimorfismo, no es solo cualquier DoFoo que está llamando, sino el DoFoo de una interfaz en particular.

En cuanto a su código de muestra ... su GetSomeText debería ser virtual ... override en el caso del polimorfismo.

No es necesario hacer referencia al recuento solo porque está utilizando el borrado de tipo. No es necesario no usar el recuento de referencias solo porque está utilizando polimorfismo.

Su Object podría incluir T* s como la forma en que almacenó los vector de los punteros en bruto en el otro caso, con la destrucción manual de su contenido (equivalente a tener que llamar a eliminar). Su Object podría envolver un std::shared_ptr<T> , y en el otro caso podría tener un vector de std::shared_ptr<T> . Su Object podría contener un std::unique_ptr<T> , equivalente a tener un vector de std::unique_ptr<T> en el otro caso. El ObjectModel su Object podría extraer constructores de copia y operadores de asignación de la T y exponerlos a Object , permitiendo una semántica de valor completo para su Object , que corresponde al vector de T en su caso de polimorfismo.


Un beneficio para los genéricos de tiempo de ejecución que nadie ha mencionado aquí (?) Es la posibilidad de que el código se genere e inyecte en una aplicación en ejecución, para usar la misma List , Hashmap / Dictionary etc., que todo lo demás en esa aplicación ya esté usando . Por qué querrías hacer eso, es otra pregunta.