Tengo una clase que adapta std :: vector para modelar un contenedor de objetos específicos del dominio. Quiero exponer la mayor parte de la API std :: vector al usuario, para que pueda usar métodos familiares (tamaño, claro, at, etc.) y algoritmos estándar en el contenedor. Este parece ser un patrón recurrente para mí en mis diseños:
class MyContainer : public std::vector<MyObject>
{
public:
// Redeclare all container traits: value_type, iterator, etc...
// Domain-specific constructors
// (more useful to the user than std::vector ones...)
// Add a few domain-specific helper methods...
// Perhaps modify or hide a few methods (domain-related)
};
Soy consciente de la práctica de preferir la composición a la herencia cuando se reutiliza una clase para la implementación, ¡pero debe haber un límite! Si tuviera que delegar todo en std :: vector, ¡habría (según mi cuenta) 32 funciones de reenvío!
Entonces, mis preguntas son ... ¿Es realmente tan malo heredar la implementación en tales casos? ¿Cuáles son los riesgos? ¿Existe una forma más segura de implementar esto sin tener que escribir tanto? ¿Soy un hereje por usar la herencia de implementación? :)
Editar:
¿Qué hay de dejar en claro que el usuario no debe usar MyContainer a través de un puntero std :: vector <>:
// non_api_header_file.h
namespace detail
{
typedef std::vector<MyObject> MyObjectBase;
}
// api_header_file.h
class MyContainer : public detail::MyObjectBase
{
// ...
};
Las bibliotecas de impulso parecen hacer estas cosas todo el tiempo.
Edición 2:
Una de las sugerencias fue utilizar funciones gratuitas. Lo mostraré aquí como pseudocódigo:
typedef std::vector<MyObject> MyCollection;
void specialCollectionInitializer(MyCollection& c, arguments...);
result specialCollectionFunction(const MyCollection& c);
etc...
Una forma más OO de hacerlo:
typedef std::vector<MyObject> MyCollection;
class MyCollectionWrapper
{
public:
// Constructor
MyCollectionWrapper(arguments...) {construct coll_}
// Access collection directly
MyCollection& collection() {return coll_;}
const MyCollection& collection() const {return coll_;}
// Special domain-related methods
result mySpecialMethod(arguments...);
private:
MyCollection coll_;
// Other domain-specific member variables used
// in conjunction with the collection.
}
Respuestas:
El riesgo es desasignar mediante un puntero a la clase base ( eliminar , eliminar [] y potencialmente otros métodos de desasignación). Dado que estas clases ( deque , map , string , etc.) no tienen dtors virtuales, es imposible limpiarlas correctamente con solo un puntero a esas clases:
struct BadExample : vector<int> {}; int main() { vector<int>* p = new BadExample(); delete p; // this is Undefined Behavior return 0; }
Dicho esto, si está dispuesto a asegurarse de no hacer esto accidentalmente, heredarlos tiene un pequeño inconveniente importante, pero en algunos casos es un gran si. Otros inconvenientes incluyen chocar con las especificaciones y extensiones de implementación (algunas de las cuales pueden no usar identificadores reservados) y lidiar con interfaces infladas ( cadenas en particular). Sin embargo, la herencia está pensada en algunos casos, ya que los adaptadores de contenedores como la pila tienen un miembro protegido c (el contenedor subyacente al que adaptan), y casi solo se puede acceder a él desde una instancia de clase derivada.
En lugar de herencia o composición, considere escribir funciones libres que tomen un par de iteradores o una referencia de contenedor y operen sobre eso. Prácticamente todo <algorithm> es un ejemplo de esto; y make_heap , pop_heap y push_heap , en particular, son un ejemplo del uso de funciones gratuitas en lugar de un contenedor específico de dominio.
Por lo tanto, use las clases de contenedor para sus tipos de datos y aún llame a las funciones gratuitas para su lógica específica de dominio. Pero aún puede lograr cierta modularidad usando un typedef, lo que le permite simplificar su declaración y proporciona un solo punto si parte de ellos necesita cambiar:
typedef std::deque<int, MyAllocator> Example; // ... Example c (42); example_algorithm(c); example_algorithm2(c.begin() + 5, c.end() - 5); Example::iterator i; // nested types are especially easier
Note que value_type y allocator pueden cambiar sin afectar el código posterior usando typedef, e incluso el contenedor puede cambiar de una deque a un vector .
fuente
Puede combinar la herencia privada y la palabra clave 'using' para solucionar la mayoría de los problemas mencionados anteriormente: La herencia privada es 'se implementa en términos de' y, como es privada, no puede mantener un puntero a la clase base
#include <string> #include <iostream> class MyString : private std::string { public: MyString(std::string s) : std::string(s) {} using std::string::size; std::string fooMe(){ return std::string("Foo: ") + *this; } }; int main() { MyString s("Hi"); std::cout << "MyString.size(): " << s.size() << std::endl; std::cout << "MyString.fooMe(): " << s.fooMe() << std::endl; }
fuente
private
herencia sigue siendo herencia y, por tanto, una relación más fuerte que la composición. En particular, significa que cambiar la implementación de su clase necesariamente romperá la compatibilidad binaria.Como todos ya han dicho, los contenedores STL no tienen destructores virtuales, por lo que heredarlos no es seguro en el mejor de los casos. Siempre he considerado la programación genérica con plantillas como un estilo diferente de OO, uno sin herencia. Los algoritmos definen la interfaz que requieren. Es lo más parecido a Duck Typing que puede conseguir en un lenguaje estático.
De todos modos, tengo algo que agregar a la discusión. La forma en que he creado mis propias especializaciones de plantilla anteriormente es definir clases como las siguientes para usar como clases base.
template <typename Container> class readonly_container_facade { public: typedef typename Container::size_type size_type; typedef typename Container::const_iterator const_iterator; virtual ~readonly_container_facade() {} inline bool empty() const { return container.empty(); } inline const_iterator begin() const { return container.begin(); } inline const_iterator end() const { return container.end(); } inline size_type size() const { return container.size(); } protected: // hide to force inherited usage only readonly_container_facade() {} protected: // hide assignment by default readonly_container_facade(readonly_container_facade const& other): : container(other.container) {} readonly_container_facade& operator=(readonly_container_facade& other) { container = other.container; return *this; } protected: Container container; }; template <typename Container> class writable_container_facade: public readable_container_facade<Container> { public: typedef typename Container::iterator iterator; writable_container_facade(writable_container_facade& other) readonly_container_facade(other) {} virtual ~writable_container_facade() {} inline iterator begin() { return container.begin(); } inline iterator end() { return container.end(); } writable_container_facade& operator=(writable_container_facade& other) { readable_container_facade<Container>::operator=(other); return *this; } };
Estas clases exponen la misma interfaz que un contenedor STL. Me gustó el efecto de separar las operaciones de modificación y no modificación en distintas clases base. Esto tiene un efecto realmente agradable sobre la corrección de la constante. El único inconveniente es que debe ampliar la interfaz si desea utilizarlos con contenedores asociativos. Aunque no me he encontrado con la necesidad.
fuente
En este caso, heredar es una mala idea: los contenedores STL no tienen destructores virtuales, por lo que es posible que se produzcan pérdidas de memoria (además, es una indicación de que los contenedores STL no están destinados a ser heredados en primer lugar).
Si solo necesita agregar alguna funcionalidad, puede declararla en métodos globales o en una clase liviana con un puntero / referencia de miembro de contenedor. Esto, por supuesto, no le permite ocultar métodos: si eso es realmente lo que busca, entonces no hay otra opción que volver a declarar toda la implementación.
fuente
-Dprivate=public
en la línea de comandos del compilador. Los especificadores de acceso como privado son en su mayoría documentación, que resulta que se hace cumplir.Dejando a un lado los dtors virtuales, la decisión de heredar versus contener debe ser una decisión de diseño basada en la clase que está creando. Nunca debe heredar la funcionalidad del contenedor solo porque es más fácil que contener un contenedor y agregar algunas funciones de agregar y eliminar que parecen envoltorios simplistas a menos que pueda decir definitivamente que la clase que está creando es una especie de contenedor. Por ejemplo, una clase de aula a menudo contendrá objetos de estudiantes, pero un aula no es una especie de lista de estudiantes para la mayoría de los propósitos, por lo que no debería heredar de la lista.
fuente
Es más fácil de hacer:
typedef std::vector<MyObject> MyContainer;
fuente
De todos modos, los métodos de reenvío se eliminarán. No obtendrá un mejor rendimiento de esta manera. De hecho, es probable que obtenga un peor rendimiento.
fuente