tipos sencillos relacionados programacion polimorfismo herencia ejemplos conceptos con caracteristicas c++ c++11 polymorphism containers heterogeneous

c++ - sencillos - polimorfismo java caracteristicas



Polimorfismo ad hoc y contenedores heterogéneos con semántica de valor (5)

Diferentes alternativas

Es posible. Hay varios enfoques alternativos a su problema. Cada uno tiene diferentes ventajas e inconvenientes (explicaré cada uno):

  1. Cree una interfaz y tenga una clase de plantilla que implemente esta interfaz para diferentes tipos. Debería apoyar la clonación.
  2. Use boost::variant y visitas.

Combinando polimorfismo estático y dinámico

Para la primera alternativa, necesitas crear una interfaz como esta:

class UsableInterface { public: virtual ~UsableInterface() {} virtual void use() = 0; virtual std::unique_ptr<UsableInterface> clone() const = 0; };

Obviamente, no desea implementar esta interfaz a mano cada vez que tenga un nuevo tipo que tenga la función use() . Por lo tanto, tengamos una clase de plantilla que lo haga por usted.

template <typename T> class UsableImpl : public UsableInterface { public: template <typename ...Ts> UsableImpl( Ts&&...ts ) : t( std::forward<Ts>(ts)... ) {} virtual void use() override { use( t ); } virtual std::unique_ptr<UsableInterface> clone() const override { return std::make_unique<UsableImpl<T>>( t ); // This is C++14 // This is the C++11 way to do it: // return std::unique_ptr<UsableImpl<T> >( new UsableImpl<T>(t) ); } private: T t; };

Ahora ya puedes hacer todo lo que necesites con él. Puedes poner estas cosas en un vector:

std::vector<std::unique_ptr<UsableInterface>> usables; // fill it

Y puedes copiar ese vector preservando los tipos subyacentes:

std::vector<std::unique_ptr<UsableInterface>> copies; std::transform( begin(usables), end(usables), back_inserter(copies), []( const std::unique_ptr<UsableInterface> & p ) { return p->clone(); } );

Probablemente no quieras ensuciar tu código con cosas como esta. Lo que quieres escribir es

copies = usables;

Bueno, puede obtener esa conveniencia envolviendo el std::unique_ptr en una clase que admita la copia.

class Usable { public: template <typename T> Usable( T t ) : p( std::make_unique<UsableImpl<T>>( std::move(t) ) ) {} Usable( const Usable & other ) : p( other.clone() ) {} Usable( Usable && other ) noexcept : p( std::move(other.p) ) {} void swap( Usable & other ) noexcept { p.swap(other.p); } Usable & operator=( Usable other ) { swap(other); } void use() { p->use(); } private: std::unique_ptr<UsableInterface> p; };

Debido a la buena constructora con plantillas, ahora puedes escribir cosas como

Usable u1 = 5; Usable u2 = std::string("Hello usable!");

Y puede asignar valores con una semántica de valor adecuada:

u1 = u2;

Y puedes poner Usables en std::vector

std::vector<Usable> usables; usables.emplace_back( std::string("Hello!") ); usables.emplace_back( 42 );

y copia ese vector

const auto copies = usables;

Puede encontrar esta idea en los Padres de Sean que hablan sobre Semántica de valores y Polimorfismo basado en conceptos . También dio una versión muy breve de esta charla en Going Native 2013 , pero creo que esto es muy rápido de seguir.

Además, puede adoptar un enfoque más genérico que escribir su propia clase Usable y reenviar todas las funciones miembro (si desea agregar otra más adelante). La idea es reemplazar la clase Usable con una clase de plantilla. Esta clase de plantilla no proporcionará una función miembro use() sino un operator T&() y operator const T&() const . Esto le da la misma funcionalidad, pero no necesita escribir una clase de valor extra cada vez que facilita este patrón.

Un contenedor sindical discriminado seguro, genérico y basado en la pila

La clase de plantilla boost::variant es exactamente eso y proporciona algo así como una union estilo C pero segura y con una semántica de valor apropiado. La forma de usarlo es esta:

using Usable = boost::variant<int,std::string,A>; Usable usable;

Puede asignar objetos de cualquiera de estos tipos a Usable .

usable = 1; usable = "Hello variant!"; usable = A();

Si todos los tipos de plantilla tienen semántica de valores, entonces boost::variant también tiene semántica de valores y puede colocarse en contenedores STL. Puede escribir una función use() para dicho objeto mediante un patrón que se denomina patrón de visitante . Llama a la función de use() correcto use() para el objeto contenido dependiendo del tipo interno.

class UseVisitor : public boost::static_visitor<void> { public: template <typename T> void operator()( T && t ) { use( std::forward<T>(t) ); } } void use( const Usable & u ) { boost::apply_visitor( UseVisitor(), u ); }

Ahora puedes escribir

Usable u = "Hello"; use( u );

Y, como ya mencioné, puedes poner estas cosas en contenedores STL.

std::vector<Usable> usables; usables.emplace_back( 5 ); usables.emplace_back( "Hello world!" ); const auto copies = usables;

Las compensaciones

Puede hacer crecer la funcionalidad en dos dimensiones:

  • Agregue nuevas clases que satisfagan la interfaz estática.
  • Agregue nuevas funciones que las clases deben implementar.

En el primer enfoque que presenté es más fácil agregar nuevas clases. El segundo enfoque hace que sea más fácil agregar nuevas funcionalidades.

En el primer enfoque, es imposible (o al menos difícil) que el código del cliente agregue nuevas funciones. En el segundo enfoque, es imposible (o al menos difícil) que el código del cliente agregue nuevas clases a la mezcla. Una salida es el llamado patrón de visitantes acíclicos que hace posible que los clientes amplíen una jerarquía de clases con nuevas clases y nuevas funcionalidades. El inconveniente aquí es que debe sacrificar una cierta cantidad de comprobación estática en tiempo de compilación. Aquí hay un enlace que describe el patrón del visitante incluyendo el patrón de visitante acíclico junto con algunas otras alternativas. Si tienes preguntas sobre esto, estoy dispuesto a responder.

Ambos enfoques son súper seguros para tipos. No hay compensación para hacer allí.

Los costos de tiempo de ejecución del primer enfoque pueden ser mucho más altos, ya que hay una asignación de montón involucrada para cada elemento que cree. El enfoque boost::variant se basa en la pila y, por lo tanto, es probablemente más rápido. Si el rendimiento es un problema con el primer enfoque, considere cambiar al segundo.

Tengo una cantidad de tipos no relacionados que soportan las mismas operaciones a través de funciones libres sobrecargadas (polimorfismo ad hoc):

struct A {}; void use(int x) { std::cout << "int = " << x << std::endl; } void use(const std::string& x) { std::cout << "string = " << x << std::endl; } void use(const A&) { std::cout << "class A" << std::endl; }

Como el título de la pregunta implica, quiero almacenar instancias de esos tipos en un contenedor heterogéneo para que pueda use() ellos sin importar qué tipo concreto sean. El contenedor debe tener una semántica de valores (es decir, una asignación entre dos contenedores copia los datos, no los comparte).

std::vector<???> items; items.emplace_back(3); items.emplace_back(std::string{ "hello" }); items.emplace_back(A{}); for (const auto& item: items) use(item); // or better yet use(items);

Y, por supuesto, esto debe ser totalmente extensible. Piense en una API de biblioteca que toma un vector<???> y un código de cliente que agrega sus propios tipos a los ya conocidos.

La solución habitual es almacenar punteros (inteligentes) en una interfaz (abstracta) (por ejemplo, vector<unique_ptr<IUsable>> ) pero esto tiene una serie de inconvenientes: desde lo más alto de mi cabeza:

  • Tengo que migrar mi modelo polimórfico ad hoc actual a una jerarquía de clases donde cada clase hereda de la interfaz común. Oh chasquido! Ahora tengo que escribir wrappers para int y string y lo que no ... Sin mencionar la menor capacidad de reutilización / compibilidad debido a que las funciones de miembros libres están íntimamente ligadas a la interfaz (funciones de miembros virtuales).
  • El contenedor pierde su semántica de valor: una asignación simple vec1 = vec2 es imposible si usamos unique_ptr (forzándonos a realizar copias profundas manualmente), o ambos contenedores terminan en estado compartido si usamos shared_ptr (que tiene sus ventajas y desventajas - pero como quiero semántica de valores en el contenedor, de nuevo me veo obligado a realizar copias profundas de forma manual).
  • Para poder realizar copias en profundidad, la interfaz debe admitir una función de clone() virtual clone() que debe implementarse en cada clase derivada. ¿Puedes pensar seriamente en algo más aburrido que eso?

Para resumir: esto agrega mucho acoplamiento innecesario y requiere toneladas de código repetitivo (posiblemente inútil). Esto definitivamente no es satisfactorio, pero hasta ahora esta es la única solución práctica que conozco.

He estado buscando una alternativa viable al subtipo de polimorfismo (también conocido como herencia de interfaz) por edades. Juego mucho con el polimorfismo ad hoc (también conocido como funciones gratuitas sobrecargadas) pero siempre toco la misma pared dura: los contenedores tienen que ser homogéneos, por lo que siempre vuelvo de mala gana a la herencia y a los indicadores inteligentes, con todos los inconvenientes ya mencionados ( y probablemente más).

Idealmente, me gustaría tener un simple vector<IUsable> con una semántica de valor adecuada, sin cambiar nada a mi jerarquía de tipos actual (ausencia de), y mantener el polimorfismo ad hoc en lugar de requerir un polimorfismo de subtipo.

es posible? ¿Si es así, cómo?


Heres una idea que obtuve recientemente de la implementación de std::function en libstdc ++:

Cree una clase de plantilla Handler<T> con una función de miembro estático que sepa cómo copiar, eliminar y realizar otras operaciones en T.

Luego, guarde un puntero de función para esa función estática en el constructor de su clase Any. Su clase Any no necesita saber acerca de T, solo necesita este puntero de función para despachar las operaciones específicas de T. Observe que la firma de la función es independiente de T.

Más o menos así:

struct Foo { ... } struct Bar { ... } struct Baz { ... } template<class T> struct Handler { static void action(Ptr data, EActions eAction) { switch (eAction) { case COPY: call T::T(...); case DELETE: call T::~T(); case OTHER: call T::whatever(); } } } struct Any { Ptr handler; Ptr data; template<class T> Any(T t) : handler(Handler<T>::action) , data(handler(t, COPY)) {} Any(const Any& that) : handler(that.handler) , data(handler(that.data, COPY)) {} ~Any() { handler(data, DELETE); } }; int main() { vector<Any> V; Foo foo; Bar bar; Baz baz; v.push_back(foo); v.push_back(bar); v.push_back(baz); }

Esto le proporciona borrado de tipo al tiempo que mantiene la semántica de valores, y no requiere modificación de las clases contenidas (Foo, Bar, Baz), y no usa polimorfismo dinámico en absoluto. Son cosas geniales.


Las otras respuestas anteriores (use la clase base de la interfaz vtabled, use boost :: variant, utilice trucos de herencia de clase base virtual) son soluciones perfectamente válidas y válidas para este problema, cada una con un saldo diferenciado de tiempos de compilación frente a costos de tiempo de ejecución. Sin embargo, sugeriría que en lugar de boost :: variant, en C ++ 11 y luego use eggs :: variant en su lugar, que es una reimplementación de boost :: variant utilizando C ++ 11/14 y es enormemente superior en diseño, rendimiento y facilidad de uso , poder de abstracción e incluso proporciona un subconjunto de características bastante completo en VS2013 (y un conjunto completo de características en VS2015). También está escrito y mantenido por un autor principal de Boost.

Sin embargo, si puede redefinir un poco el problema, específicamente, que puede perder el tipo que borra std :: vector en favor de algo mucho más poderoso, podría usar contenedores de tipo heterogéneo. Estos funcionan devolviendo un nuevo tipo de contenedor para cada modificación del contenedor, por lo que el patrón debe ser:

newtype newcontainer = oldcontainer.push_back (newitem);

Estos fueron un dolor para usar en C ++ 03, aunque Boost.Fusion hace un buen puño para hacerlos potencialmente útiles. La usabilidad realmente útil solo es posible desde C ++ 11 en adelante, y especialmente desde C ++ 14 gracias a lambdas genéricas que hacen que trabajar con estas heterogéneas colecciones sea muy sencillo programar usando la programación funcional constexpr, y probablemente la biblioteca líder actual de kits de herramientas para eso es ahora propuso Boost.Hana que idealmente requiere clang 3.6 o GCC 5.0.

Los contenedores de tipo heterogéneo son prácticamente el 99% de tiempo de compilación 1% de solución de costo de tiempo de ejecución. Verás una gran cantidad de optimizadores de compiladores con la tecnología actual de compiladores, por ejemplo, una vez vi clang 3.5 generar 2500 códigos de operación para el código que debería haber generado dos códigos de operación, y para el mismo código GCC 4.9 escupió 15 códigos de operación 12 de los cuales no de hecho hacen cualquier cosa (cargaron la memoria en los registros y no hicieron nada con esos registros). Dicho todo esto, dentro de unos años podrá lograr una generación de código óptima para contenedores de tipo heterogéneo, en cuyo punto esperaría que se convirtieran en la próxima generación de metaprogramación de C ++ en la que, en lugar de buscar plantillas, lo hagamos. ¡Ser capaz de programar funcionalmente el compilador C ++ usando funciones reales!


Tal vez impulso :: variante?

#include <iostream> #include <string> #include <vector> #include "boost/variant.hpp" struct A {}; void use(int x) { std::cout << "int = " << x << std::endl; } void use(const std::string& x) { std::cout << "string = " << x << std::endl; } void use(const A&) { std::cout << "class A" << std::endl; } typedef boost::variant<int,std::string,A> m_types; class use_func : public boost::static_visitor<> { public: template <typename T> void operator()( T & operand ) const { use(operand); } }; int main() { std::vector<m_types> vec; vec.push_back(1); vec.push_back(2); vec.push_back(std::string("hello")); vec.push_back(A()); for (int i=0;i<4;++i) boost::apply_visitor( use_func(), vec[i] ); return 0; }

Ejemplo en vivo: http://coliru.stacked-crooked.com/a/e4f4ccf6d7e6d9d8


Crédito donde es debido: cuando vi la charla de Going Native 2013 "La herencia es la clase base del mal" de Sean Parent , me di cuenta de lo simple que era, en retrospectiva, resolver este problema. Solo puedo aconsejarle que lo mire (hay mucho más cosas interesantes empaquetadas en solo 20 minutos, esta Q / A apenas araña la superficie de toda la charla), así como las otras conversaciones de Going Native 2013 .

En realidad es tan simple que apenas necesita explicación, el código habla por sí mismo:

struct IUsable { template<typename T> IUsable(T value) : m_intf{ new Impl<T>(std::move(value)) } {} IUsable(IUsable&&) noexcept = default; IUsable(const IUsable& other) : m_intf{ other.m_intf->clone() } {} IUsable& operator =(IUsable&&) noexcept = default; IUsable& operator =(const IUsable& other) { m_intf = other.m_intf->clone(); return *this; } // actual interface friend void use(const IUsable&); private: struct Intf { virtual ~Intf() = default; virtual std::unique_ptr<Intf> clone() const = 0; // actual interface virtual void intf_use() const = 0; }; template<typename T> struct Impl : Intf { Impl(T&& value) : m_value(std::move(value)) {} virtual std::unique_ptr<Intf> clone() const override { return std::unique_ptr<Intf>{ new Impl<T>(*this) }; } // actual interface void intf_use() const override { use(m_value); } private: T m_value; }; std::unique_ptr<Intf> m_intf; }; // ad hoc polymorphic interface void use(const IUsable& intf) { intf.m_intf->intf_use(); } // could be further generalized for any container but, hey, you get the drift template<typename... Args> void use(const std::vector<IUsable, Args...>& c) { std::cout << "vector<IUsable>" << std::endl; for (const auto& i: c) use(i); std::cout << "End of vector" << std::endl; } int main() { std::vector<IUsable> items; items.emplace_back(3); items.emplace_back(std::string{ "world" }); items.emplace_back(items); // copy "items" in its current state items[0] = std::string{ "hello" }; items[1] = 42; items.emplace_back(A{}); use(items); } // vector<IUsable> // string = hello // int = 42 // vector<IUsable> // int = 3 // string = world // End of vector // class A // End of vector

Como puede ver, se trata de un contenedor bastante simple alrededor de una unique_ptr<Interface> , con un constructor con plantilla que crea una Implementation<T> derivada Implementation<T> . Todos los detalles (no del todo) sangrientos son privados, la interfaz pública no podría ser más clara: el contenedor no tiene funciones miembro excepto construcción / copia / movimiento, la interfaz se proporciona como una función de use() libre use() que sobrecarga el existente unos.

Obviamente, la elección de unique_ptr significa que debemos implementar una función privada clone() que se llama cada vez que queremos hacer una copia de un objeto IUsable (que a su vez requiere una asignación de montón). Es cierto que una asignación de montón por copia es bastante subóptima, pero este es un requisito si cualquier función de la interfaz pública puede mutar el objeto subyacente (es decir, si use() tomó referencias no constantes y las modificó): de esta manera nos aseguramos de que cada el objeto es único y por lo tanto puede ser mutado libremente.

Ahora bien, si, como en la pregunta, los objetos son completamente inmutables (no solo a través de la interfaz expuesta, téngalo en cuenta, realmente quiero decir que los objetos completos son siempre y completamente inmutables ), entonces podemos introducir un estado compartido sin efectos secundarios nefastos. La forma más sencilla de hacerlo es usar un shared_ptr - to-const en lugar de un unique_ptr :

struct IUsableImmutable { template<typename T> IUsableImmutable(T value) : m_intf(std::make_shared<const Impl<T>>(std::move(value))) {} IUsableImmutable(IUsableImmutable&&) noexcept = default; IUsableImmutable(const IUsableImmutable&) noexcept = default; IUsableImmutable& operator =(IUsableImmutable&&) noexcept = default; IUsableImmutable& operator =(const IUsableImmutable&) noexcept = default; // actual interface friend void use(const IUsableImmutable&); private: struct Intf { virtual ~Intf() = default; // actual interface virtual void intf_use() const = 0; }; template<typename T> struct Impl : Intf { Impl(T&& value) : m_value(std::move(value)) {} // actual interface void intf_use() const override { use(m_value); } private: const T m_value; }; std::shared_ptr<const Intf> m_intf; }; // ad hoc polymorphic interface void use(const IUsableImmutable& intf) { intf.m_intf->intf_use(); } // could be further generalized for any container but, hey, you get the drift template<typename... Args> void use(const std::vector<IUsableImmutable, Args...>& c) { std::cout << "vector<IUsableImmutable>" << std::endl; for (const auto& i: c) use(i); std::cout << "End of vector" << std::endl; }

Observe cómo la función clone() ha desaparecido (no la necesitamos más, solo compartimos el objeto subyacente y no es molesto, ya que es inmutable), y cómo ahora es la copia noexcept gracias a shared_ptr garantías de shared_ptr .

La parte divertida es que los objetos subyacentes tienen que ser inmutables, pero aún puedes mutar su envoltura IUsableImmutable por lo que todavía está perfectamente bien hacer esto:

std::vector<IUsableImmutable> items; items.emplace_back(3); items[0] = std::string{ "hello" };

(solo el shared_ptr está mutado, no el objeto subyacente en sí mismo, por lo que no afecta a las otras referencias compartidas)