c++ - implement - Subclase/heredar contenedores estándar?
interface c++ (9)
Debe abstenerse de derivar públicamente de contianers estándar. Puede elegir entre la herencia privada y la composición, y me parece que todas las pautas generales indican que la composición es mejor aquí ya que no anula ninguna función. No se deriva públicamente de contenedores STL , realmente no hay necesidad de ello.
Por cierto, si desea agregar un conjunto de algoritmos al contenedor, considere agregarlos como funciones independientes tomando un rango de iterador.
A menudo leo estas declaraciones en Stack Overflow. Personalmente, no encuentro ningún problema con esto, a menos que lo esté usando de forma polimórfica; es decir, donde tengo que usar virtual
destructor virtual
.
Si quiero ampliar / agregar la funcionalidad de un contenedor estándar, ¿cuál es una mejor manera de heredar uno? Envolver esos contenedores dentro de una clase personalizada requiere mucho más esfuerzo y aún no está limpio.
El problema es que usted, u otra persona, podría pasar accidentalmente su clase extendida a una función esperando una referencia a la clase base. Eso efectivamente (¡y silenciosamente!) Cortará las extensiones y creará algunos errores difíciles de encontrar.
Tener que escribir algunas funciones de reenvío parece un pequeño precio a pagar en comparación.
En mi humilde opinión, no encuentro ningún daño en la herencia de contenedores STL si se utilizan como extensiones de funcionalidad . (Es por eso que hice esta pregunta :))
El posible problema puede ocurrir cuando intenta pasar el puntero / referencia de su contenedor personalizado a un contenedor estándar.
template<typename T>
struct MyVector : std::vector<T> {};
std::vector<int>* p = new MyVector<int>;
//....
delete p; // oops "Undefined Behavior"; as vector::~vector() is not ''virtual''
Tales problemas se pueden evitar conscientemente , siempre que se sigan buenas prácticas de programación.
Si quiero tener un cuidado extremo , puedo ir hasta esto:
#include<vector>
template<typename T>
struct MyVector : std::vector<T> {};
#define vector DONT_USE
Lo cual no permitirá el uso del vector
completo.
Hay una serie de razones por las cuales esta es una mala idea.
Primero, esta es una mala idea porque los contenedores estándar no tienen destructores virtuales . Nunca debe usar algo polimórficamente que no tenga destructores virtuales, porque no puede garantizar la limpieza en su clase derivada.
Reglas básicas para dtors virtuales
En segundo lugar, es realmente un mal diseño. Y en realidad hay varias razones por las cuales es un mal diseño. En primer lugar, siempre debe ampliar la funcionalidad de los contenedores estándar a través de algoritmos que operan de manera genérica. Esta es una simple razón de complejidad: si tiene que escribir un algoritmo para cada contenedor al que se aplica y tiene M contenedores y N algoritmos, es decir M x N métodos que debe escribir. Si escribe sus algoritmos genéricamente, solo tiene algoritmos N. Entonces obtienes mucho más reutilización.
También es un diseño realmente malo porque estás rompiendo una buena encapsulación al heredar del contenedor. Una buena regla empírica es: si puede realizar lo que necesita utilizando la interfaz pública de un tipo, establezca ese nuevo comportamiento externo al tipo. Esto mejora la encapsulación. Si desea implementar un comportamiento nuevo, conviértalo en una función de ámbito de espacio de nombre (como los algoritmos). Si tiene una nueva invariante para imponer, use la contención en una clase.
Una descripción clásica de encapsulación
Finalmente, en general, nunca debería pensar en la herencia como un medio para extender el comportamiento de una clase. Esta es una de las grandes y malas mentiras de la teoría de la POO inicial que surgió debido a un pensamiento poco claro sobre la reutilización, y continúa siendo enseñada y promovida hasta el día de hoy a pesar de que existe una teoría clara de por qué es mala. Cuando utiliza la herencia para extender el comportamiento, está vinculando ese comportamiento extendido a su contrato de interfaz de una manera que vincule las manos de los usuarios con los cambios futuros. Por ejemplo, supongamos que tiene una clase de tipo Socket que se comunica utilizando el protocolo TCP y amplía su comportamiento derivando una clase SSLSocket de Socket e implementando el comportamiento del protocolo de pila SSL superior sobre Socket. Ahora, supongamos que obtiene un nuevo requisito para tener el mismo protocolo de comunicaciones, pero a través de una línea USB o por telefonía. Tendría que cortar y pegar todo ese trabajo en una nueva clase que se deriva de una clase USB o una clase de telefonía. Y ahora, si encuentra un error, debe solucionarlo en los tres lugares, lo que no siempre sucederá, lo que significa que los errores tardarán más tiempo y no siempre se solucionan ...
Esto es general para cualquier jerarquía de herencia A-> B-> C -> ... Cuando quiera usar los comportamientos que ha extendido en clases derivadas, como B, C, .. en objetos que no sean de la clase base A, tienes que rediseñar o estás duplicando la implementación. Esto lleva a diseños muy monolíticos que son muy difíciles de cambiar en el futuro (piense en el MFC de Microsoft o su .NET, o bien, cometen un error enorme). En cambio, casi siempre debes pensar en la extensión a través de la composición siempre que sea posible. La herencia debe usarse cuando piense en "Principio abierto / cerrado". Debe tener clases base abstractas y tiempo de ejecución de polimorfismo dinámico a través de clases heredadas, cada una tendrá implementaciones completas. Las jerarquías no deberían ser profundas, casi siempre dos niveles. Solo use más de dos cuando tenga diferentes categorías dinámicas que vayan a una variedad de funciones que necesitan esa distinción para la seguridad del tipo. En esos casos, use bases abstractas hasta las clases de hojas, que tienen la implementación.
La herencia pública es un problema por todas las razones que otros han expresado, a saber, que su contenedor puede enviarse a la clase base que no tiene un destructor virtual o un operador de asignación virtual, lo que puede provocar problemas de corte .
Privar de herencia, por otro lado, es un problema menor. Considere el siguiente ejemplo:
#include <vector>
#include <iostream>
// private inheritance, nobody else knows about the inheritance, so nobody is upcasting my
// container to a std::vector
template <class T> class MyVector : private std::vector<T>
{
private:
// in case I changed to boost or something later, I don''t have to update everything below
typedef std::vector<T> base_vector;
public:
typedef typename base_vector::size_type size_type;
typedef typename base_vector::iterator iterator;
typedef typename base_vector::const_iterator const_iterator;
using base_vector::operator[];
using base_vector::begin;
using base_vector::clear;
using base_vector::end;
using base_vector::erase;
using base_vector::push_back;
using base_vector::reserve;
using base_vector::resize;
using base_vector::size;
// custom extension
void reverse()
{
std::reverse(this->begin(), this->end());
}
void print_to_console()
{
for (auto it = this->begin(); it != this->end(); ++it)
{
std::cout << *it << ''/n'';
}
}
};
int main(int argc, char** argv)
{
MyVector<int> intArray;
intArray.resize(10);
for (int i = 0; i < 10; ++i)
{
intArray[i] = i + 1;
}
intArray.print_to_console();
intArray.reverse();
intArray.print_to_console();
for (auto it = intArray.begin(); it != intArray.end();)
{
it = intArray.erase(it);
}
intArray.print_to_console();
return 0;
}
SALIDA:
1
2
3
4
5
6
7
8
9
10
10
9
8
7
6
5
4
3
2
1
Limpio y simple, y le da la libertad de extender contenedores estándar sin mucho esfuerzo.
Y si piensas en hacer algo tonto, así:
std::vector<int>* stdVector = &intArray;
Obtienes esto:
error C2243: ''type cast'': conversion from ''MyVector<int> *'' to ''std::vector<T,std::allocator<_Ty>> *'' exists, but is inaccessible
La razón más común para querer heredar de los contenedores es porque desea agregar alguna función de miembro a la clase. Como stdlib en sí mismo no es modificable, se cree que la herencia es el sustituto. Esto no funciona sin embargo. Es mejor hacer una función gratuita que tome un vector como parámetro:
void f(std::vector<int> &v) { ... }
Ocasionalmente heredo de los tipos de colección simplemente como una mejor forma de nombrar los tipos.
No me gusta typedef
como una cuestión de preferencia personal. Entonces haré algo como:
class GizmoList : public std::vector<CGizmo>
{
/* No Body & no changes. Just a more descriptive name */
};
Entonces es mucho más fácil y más claro escribir:
GizmoList aList = GetGizmos();
Si comienza a agregar métodos a GizmoList, es posible que tenga problemas.
Porque nunca puedes garantizar que no los has usado de forma polimórfica. Estás rogando por problemas. Tomar el esfuerzo de escribir algunas funciones no es gran cosa, y, bueno, incluso querer hacer esto es dudoso en el mejor de los casos. ¿Qué pasó con la encapsulación?
Puede que a muchas personas aquí no les guste esta respuesta, pero es hora de que se cuente algo de herejía y sí ... ¡que se les diga también que "el rey está desnudo"!
Toda la motivación contra la derivación es débil. La derivación no es diferente a la composición. Es solo una manera de "poner las cosas juntas". La composición pone las cosas juntas, dándoles nombres, la herencia lo hace sin dar nombres explícitos.
Si necesita un vector que tenga la misma interfaz e implementación de std :: vect y algo más, puede:
utilice la composición y reescriba todos los prototipos de función de objeto incrustado que implementan la función que los delega (y si son 10000 ... sí: prepárese para reescribir todos esos 10000) o ...
heredarlo y agregar solo lo que necesita (y ... solo reescribir constructores, hasta que los abogados de C ++ decidan dejarlos ser heredables también: todavía recuerdo la discusión fanática sobre "por qué los ctors no pueden llamarse" hace 10 años y por qué es una "cosa mala y mala" ... hasta que C ++ 11 lo permitió y de repente todos esos fanáticos se callaron!) y dejó que el nuevo destructor no fuera virtual como lo era el original.
Al igual que para todas las clases que tienen algún método virtual y otras no, usted sabe que no puede pretender invocar el método no virtual de derivado al dirigirse a la base, lo mismo se aplica para la eliminación. No hay razón para eliminar para pretender un cuidado especial en particular. Un programador que sabe que lo que no es virtual no es invocable y que aborda la base también sabe que no debe usar eliminar en su base después de asignar su derivada.
Todo el "evitar esto" "no hagas eso" siempre suena como "moralización" de algo que es nativamente agnóstico. Todas las características de un idioma existen para resolver algún problema. El hecho de que una forma determinada de resolver el problema sea buena o mala depende del contexto, no de la característica en sí misma. Si lo que estás haciendo necesita servir a muchos contenedores, la herencia probablemente no sea la mejor (debes rehacerlo todo). Si es por un caso específico ... la herencia es una forma de componer. Olvide los purismos de OOP: C ++ no es un "OOP puro" y el contenedor no es OOP en absoluto.