cplusplus - vector initialization c++
¿Por qué los contenedores STL no tienen destructores virtuales? (9)
¿Por qué los contenedores STL no fueron diseñados para permitir la herencia?
En mi humilde opinión lo son. Si no lo hicieran, se habrían convertido en definitivos . Y cuando miro en la fuente stl_vector.h
, puedo ver que mi implementación de STL usa la herencia protegida de _Vector_base<_Tp, _Alloc>
para otorgar acceso a las clases derivadas:
template<typename _Tp, typename _Alloc = allocator<_Tp> >
class vector : protected _Vector_base<_Tp, _Alloc>
¿No usaría la herencia privada si la subclasificación no fuera bienvenida?
¿Existe una forma de subclases segura para estándares con destructores no virtuales (supongamos que no quiero anular ningún método, solo que deseo agregar nuevos)?
¿Por qué no utilizar private
herencia protected
o private
y exponer la parte deseada de la interfaz con el using
palabra clave?
class MyVector : private std::vector<int>
{
typedef std::vector<int> Parent;
public:
using Parent::size;
using Parent::push_back;
using Parent::clear;
//and so on + of course required ctors, dtors and operators.
};
Este enfoque garantiza que el usuario de la clase no disminuya la instancia a std::vector<int>
y que esté seguro, ya que el único problema con el destructor no virtual es que no llamará al derivado, cuando el objeto se elimine como una instancia de clase padre.
... También he perdido la idea de que incluso puedes heredar públicamente si tu clase no tiene un destructor. ¿Herejía?
¿Alguien sabe por qué los contenedores STL no tienen destructores virtuales?
Por lo que puedo decir, los únicos beneficios son:
- reduce el tamaño de una instancia en un puntero (a la tabla de métodos virtuales) y
- hace que la destrucción y la construcción sean un poco más rápidas.
El inconveniente es que no es seguro clasificar los contenedores de la manera habitual.
EDITAR: Tal vez mi pregunta podría ser reformulada "¿Por qué los contenedores STL no fueron diseñados para permitir la herencia?"
Debido a que no admiten la herencia, uno se queda con las siguientes opciones cuando quiere tener un nuevo contenedor que necesite la funcionalidad STL más una pequeña cantidad de funciones adicionales (por ejemplo, un constructor especializado o nuevos accesores con valores predeterminados para un mapa). o lo que sea):
- Composición y replicación de la interfaz : cree una nueva plantilla o clase que sea propietaria del contenedor STL como miembro privado y que tenga un método de transferencia en línea para cada método STL. Esto es tan importante como la herencia, evita el costo de una tabla de método virtual (en los casos en que eso importa). Desafortunadamente, los contenedores STL tienen interfaces bastante amplias, por lo que esto requiere muchas líneas de código para algo que aparentemente debería ser fácil de hacer.
- Simplemente haga funciones : use funciones simples (posiblemente con plantilla) de ámbito de archivo en lugar de intentar agregar funciones miembro. En cierto modo, este puede ser un buen enfoque, pero los beneficios de la encapsulación se pierden.
- Composición con acceso de STL público : haga que el propietario del contenedor de STL permita a los usuarios acceder al mismo contenedor de STL (quizás protegido mediante accesores). Esto requiere la menor codificación para el escritor de la biblioteca, pero es mucho menos conveniente para los usuarios. Uno de los grandes puntos de venta para la composición es que reduce el acoplamiento en su código, pero esta solución combina completamente el contenedor STL con el contenedor propietario (porque el propietario devuelve un verdadero contenedor STL).
- Polimorfismo en tiempo de compilación : puede ser algo difícil de escribir, requiere algo de gimnasia de códigos y no es apropiado para todas las situaciones.
Como una pregunta adicional: ¿existe una forma segura de crear subclases con destructores no virtuales (supongamos que no quiero anular ningún método, solo que quiero agregar otros)? Mi impresión es que no hay una forma genérica y segura de hacer esto si uno no tiene el poder de cambiar el código que define la clase no virtual.
EDICION 2:
Como señala @doc , las declaraciones de using
C ++ 11 más costosas reducen un poco el costo de la composición.
Como se ha señalado, los contenedores STL no están diseñados para ser heredables. No hay métodos virtuales, todos los miembros de datos son privados, no hay getters / setters / helpers protegidos. Y como has descubierto, no hay destructores virtuales.
Le sugiero que realmente debería usar los contenedores a través de la composición en lugar de la herencia de implementación, de una manera "tiene una" en lugar de una "es una".
Creo que Stroustrup respondió indirectamente a esta pregunta en su fantástico artículo: ¿Por qué C ++ no es solo un lenguaje de programación orientado a objetos ? :
7 Observaciones finales
¿Las diversas instalaciones presentadas anteriormente están objetadas o no? ¿Cuáles? ¿Usando qué definición de orientado a objetos? En la mayoría de los contextos, creo que estas son las preguntas equivocadas. Lo que importa es qué ideas puede expresar claramente, con qué facilidad puede combinar el software de diferentes fuentes, y qué tan eficientes y sostenibles son los programas resultantes. En otras palabras, la forma en que usted respalda las buenas técnicas de programación y las buenas técnicas de diseño es más importante que las etiquetas y las palabras de moda. La idea fundamental es simplemente mejorar el diseño y la programación a través de la abstracción. Desea ocultar detalles, desea aprovechar cualquier característica común de un sistema y desea que esto sea asequible. Me gustaría animarte a no hacer un término sin sentido orientado a los objetores. La noción de '''' orientado a objetos '''' se degrada con demasiada frecuencia- equiparándolo con el bien,
- equiparándolo con un solo idioma, o
- Aceptando todo como objetivado.
He argumentado que hay, y debe haber, técnicas útiles más allá de la programación y el diseño orientados a objetos. Sin embargo, para evitar ser totalmente malentendido, me gustaría enfatizar que no intentaría un proyecto serio usando un lenguaje de programación que al menos no apoye la noción clásica de programación orientada a objetos. Además de las instalaciones que admiten la programación orientada a objetos, quiero, y C ++ proporciona, características que van más allá de las de su soporte para la expresión directa de conceptos y relaciones.
STL fue construido con tres herramientas conceptuales en mente principalmente. Programación genérica + Estilo funcional + Abstracción de datos == Estilo STL . No es extraño que la POO no sea la mejor forma de representar una biblioteca de Estructura de datos y algoritmos. Aunque la OOP se usa en otras partes de la biblioteca estándar, el diseñador de STL vio que la combinación de las tres técnicas mencionadas es mejor que la OOP sola . En resumen, la biblioteca no fue diseñada pensando en OOP, y en C ++, si no la usas, no se incluye con tu código. No pagas por lo que no usas. Las clases std :: vector, std :: list, ... no son conceptos OOP en el sentido de Java / C #. Son solo tipos abstractos de datos en la mejor interpretación.
Ningún destructor virtual impide que la clase sea subclases correctamente.
Otra solución para poder crear subclases de contenedores STL es una propuesta de Bo Qian que utiliza punteros inteligentes.
C ++ avanzado: Destructor virtual y Destructor inteligente
class Dog {
public:
~Dog() {cout << "Dog is destroyed"; }
};
class Yellowdog : public Dog {
public:
~Yellowdog() {cout << "Yellow dog destroyed." << endl; }
};
class DogFactory {
public:
static shared_ptr<Dog> createYellowDog() {
return shared_ptr<Yellowdog>(new Yellowdog());
}
};
int main() {
shared_ptr<Dog> pd = DogFactory::createYellowDog();
return 0;
}
Esto evita el dillema con destructores virtuales por completo.
Si realmente necesita un destructor virtual, puede agregarlo en la clase derivada del vector <>, y luego usar esta clase como una clase base en cualquier lugar donde necesite una interfaz virtual. Al hacer esto, el compilador llamará al destructor virtual de su clase base, que a su vez llamará al destructor no virtual de la clase vector.
Ejemplo:
#include <vector>
#include <iostream>
using namespace std;
class Test
{
int val;
public:
Test(int val) : val(val)
{
cout << "Creating Test " << val << endl;
}
Test(const Test& other) : val(other.val)
{
cout << "Creating copy of Test " << val << endl;
}
~Test()
{
cout << "Destructing Test " << val << endl;
}
};
class BaseVector : public vector<Test>
{
public:
BaseVector()
{
cout << "Creating BaseVector" << endl;
}
virtual ~BaseVector()
{
cout << "Destructing BaseVector" << endl;
}
};
class FooVector : public BaseVector
{
public:
FooVector()
{
cout << "Creating FooVector" << endl;
}
virtual ~FooVector()
{
cout << "Destructing FooVector" << endl;
}
};
int main()
{
BaseVector* ptr = new FooVector();
ptr->push_back(Test(1));
delete ptr;
return 0;
}
Este código da el siguiente resultado:
Creating BaseVector
Creating FooVector
Creating Test 1
Creating copy of Test 1
Destructing Test 1
Destructing FooVector
Destructing BaseVector
Destructing Test 1
Supongo que sigue la filosofía de C ++ de no pagar por funciones que no usas. Dependiendo de la plataforma, un puntero para la tabla virtual podría ser un precio considerable para pagar si no te importa tener un destructor virtual.
Un destructor virtual solo es útil para escenarios de herencia. Los contenedores STL no están diseñados para heredarse (ni es un escenario admitido). Por lo tanto, no tienen destructores virtuales.
no se supone que debes agregar ciegamente un destructor virtual a cada clase. Si ese fuera el caso, el idioma no le permitiría ninguna otra opción. Cuando agrega un método virtual a una clase que no tiene ningún otro método virtual, simplemente aumentó el tamaño de las instancias de clase por el tamaño de un puntero, generalmente 4 bytes. Eso es costoso dependiendo de lo que estés haciendo. El aumento de tamaño ocurre porque se crea una tabla v para contener la lista de métodos virtuales, y cada instancia necesita un puntero a la tabla v. Normalmente se encuentra en la primera celda de la instancia.