c++ inheritance c++11 factory type-erasure

c++ - Aclaración sobre la charla de Sean Parent "La herencia es la clase base del mal"



inheritance c++11 (1)

Tipo de borrado 101:

Paso 1: haga un tipo regular (o semi-regular de solo movimiento) que oculte el detalle.

struct exposed_type;

Esta clase expone los conceptos que desea apoyar. Copie, mueva, destruya, igual a, orden total, hash y / o cualquier concepto personalizado que necesite admitir.

struct exposed_type { exposed_type(exposed_type const&); exposed_type(exposed_type&&); friend bool operator<(exposed_type const&, exposed_type const&); friend std::size_t hash(exposed_type const&); // etc };

Muchos de estos conceptos pueden asignarse aproximadamente desde un método de interfaz virtual pura en su solución basada en herencia actual.

Cree métodos no virtuales en su tipo Regular que expresen los conceptos. Copiar / asignar para copiar, etc.

Paso 2: Escribe un tipo de ayudante de borrado.

struct internal_interface;

Aquí tienes interfaces virtuales puras. clone() para copia, etc.

struct internal_interface { virtual ~internal_interface() {} virtual internal_interface* clone() const = 0; virtual int cmp( internal_interface const& o ) const = 0; virtual std::size_t get_hash() const = 0; // etc virtual std::type_info const* my_type_info() const = 0; };

Almacene un puntero inteligente 1 a este en su tipo regular arriba.

struct exposed_type { std::unique_ptr<internal_interface> upImpl;

Reenvía los métodos regulares al ayudante. Por ejemplo:

exposed_type::exposed_type( exposed_type const& o ): upImpl( o.upImpl?o.upImpl->clone():nullptr ) {} exposed_type::exposed_type( exposed_type&& o )=default;

Paso 3: escribir una implementación de borrado de tipo. Esta es una clase de template que almacena una T y hereda del ayudante, y reenvía la interfaz a la T Use funciones libres (como like std::begin ) que usa métodos en la implementación predeterminada si no se encontró una función sin adl.

// used if ADL does not find a hash: template<class T> std::size_t hash( T const& t ) { return std::hash<T>{}(t); } template<class T> struct internal_impl:internal_interface { T t; virtual ~internal_impl() {} virtual internal_impl* clone() const { return new internal_impl{t}; } virtual int cmp( internal_interface const& o ) const { if (auto* po = dynamic_cast<internal_interface const*>(&o)) { if (t < *po) return -1; if (*po < t) return 1; return 0; } if (my_type_info()->before(*o.my_type_info()) return -1; if (o.my_type_info()->before(*my_type_info()) return 1; ASSERT(FALSE); return 0; } virtual std::size_t get_hash() const { return hash(t); } // etc std::type_info const* my_type_info() const { return std::addressof( typeid(T) ); // note, static type, not dynamic } };

Paso 4: agregue un constructor a su tipo normal que tome una T y construya una implementación de borrado de tipo a partir de él, y rellene eso con su puntero inteligente al ayudante.

template<class T, // SFINAE block using this ctor as a copy/move ctor: std::enable_if_t<!std::is_same<exposed_type, std::decay_t<T>>::value, int>* =nullptr > exposed_type( T&& t ): upImpl( new internal_impl<std::decay_t<T>>{std::forward<T>(t)} ) {}

Después de todo este trabajo, ahora tiene un sistema polimórfico no intrusivo con un tipo de valor regular (o semi-regular).

Sus funciones de fábrica devuelven el tipo normal.

Mire en las implementaciones de muestra de la std::function para ver esto hecho completamente.

Tanto las opciones únicas como las compartidas son buenas, dependiendo de si desea almacenar datos inmutables / copiar en escritura, o clonar manualmente.

La conversación de Sean Parent, Herencia es la clase base del mal , dice que el polimorfismo no es una propiedad del tipo, sino una propiedad de cómo se usa. Como regla general, no use la herencia para implementar interfaces. Entre los muchos beneficios de esto está la desvirtualización de las clases que tienen funciones virtuales solo porque estaban implementando una interfaz. Aquí hay un ejemplo:

class Drawable { public: virtual void draw() = 0; }; class DrawA : public Drawable { public: void draw() override{//do something} }; class UseDrawable { public: void do(){mDraw->draw();} Drawable* mDraw; };

Aquí, en lugar de que UseDrawable requiera que mDraw sea ​​un Drawable* , puede hacer que use una clase de borrado de tipo que puede envolver a cualquier clase que implemente un miembro llamado draw . Entonces, algo como un boost::type_erasure::any con la definición apropiada. De esa manera, DrawA no necesita heredar de Drawable : el polimorfismo era realmente el requisito de UseDrawable y no es realmente una propiedad de DrawA .

Estoy tratando de refactorizar algún código siguiendo este principio. Tengo una clase abstracta ModelInterface y dos clases concretas ModelA y ModelB de ModelInterface . Siguiendo el consejo de Sean, tiene sentido no forzar a ModelA y ModelB a la jerarquía de herencia y simplemente usar el borrado de tipo en ubicaciones que requieren una clase que satisfaga el concepto modelado por ModelInterface .

Ahora, mi problema es que la mayoría de los lugares en mi código que actualmente usan un ModelInterface también lo hacen construyendo un objeto apropiado basado en un archivo de configuración de tiempo de ejecución. Actualmente, la fábrica ModelInterface* un new objeto apropiado y devolverá un ModelInterface* . Si refactorizo ​​el código para usar un concepto de borrado de tipo (diga algo como boost::type_erasure::any<implement ModelInterface> ) en estas ubicaciones en el código, ¿cómo construyo tales objetos en tiempo de ejecución? ¿Serán necesarios ModelA y ModelB para ser clases habilitadas para RTTI? ¿O puedo construirlos en fábrica y usarlos sin información RTTI de alguna manera?

(Con RTTI, puedo tener una clase abstracta, digamos FactoryConstructible , y usar dynamic_cast<void*> para obtener el tipo final).