programacion - ¿Por qué no se aplica el polimorfismo en matrices en C++?
polimorfismo poo (4)
Dado,
Base* p = Derived[4];
El estándar C ++ 11 hace
delete [] p;
ser un comportamiento indefinido.
5.3.5 Eliminar
...
2 ... En la segunda alternativa (eliminar matriz) si el tipo dinámico del objeto a eliminar difiere de su tipo estático, el comportamiento no está definido.
Desde el punto de vista de un diseño de memoria, también tiene sentido por qué delete [] p;
dará lugar a un comportamiento indefinido.
Si sizeof(Derived)
es N
, el new Derived[4]
asigna memoria que será algo así como:
+--------+--------+--------+--------+
| N | N | N | N |
+--------+--------+--------+--------+
En general, sizeof(Base)
<= sizeof(Derived)
. En su caso, sizeof(Base)
< sizeof(Derived)
dado que Derived
tiene una variable miembro adicional.
Cuando usas:
Base* p = new Derived[4];
tienes:
p
|
V
+--------+--------+--------+--------+
| N | N | N | N |
+--------+--------+--------+--------+
p+1
apunta a algún lugar en medio del primer objeto desde sizeof(Base) < sizeof(Derived)
.
p+1
|
V
+--------+--------+--------+--------+
| N | N | N | N |
+--------+--------+--------+--------+
Cuando se llama al destructor en p+1
, el puntero no apunta al inicio de un objeto. Por lo tanto, el programa exhibe síntomas de comportamiento indefinido.
Un problema relacionado
Debido a las diferencias en los tamaños de Base
y Derived
, no se puede iterar sobre los elementos de la matriz asignada dinámicamente usando p
.
for ( int i = 0; i < 4; ++i )
{
// Do something with p[i]
// will not work since p+i does not necessary point to an object
// boundary.
}
Esta pregunta ya tiene una respuesta aquí:
#include <iostream>
using namespace std;
struct Base
{
virtual ~Base()
{
cout << "~Base(): " << b << endl;
}
int b = 1;
};
struct Derived : Base
{
~Derived() override
{
cout << "~Derived(): " << d << endl;
}
int d = 2;
};
int main()
{
Base* p = new Derived[4];
delete[] p;
}
La salida es como sigue: (Visual Studio 2015 con Clang 3.8)
~Base(): 1
~Base(): 2
~Base(): -2071674928
~Base(): 1
¿Por qué no se aplica el polimorfismo en matrices en C ++?
Obtiene un comportamiento indefinido porque el operador delete[]
no tiene idea de qué tipo de objeto se almacena en la matriz, por lo que confía en el tipo estático para decidir sobre las compensaciones de objetos individuales. El estándar dice lo siguiente:
En la segunda alternativa (eliminar matriz) si el tipo dinámico del objeto a eliminar difiere de su tipo estático, el comportamiento no está definido.
El tipo estático de la matriz debe coincidir con el tipo de elemento que utiliza para la asignación:
Derived* p = new Derived[4]; // Obviously, this works
Estas preguntas y respuestas incluyen más detalles sobre la razón por la cual la norma tiene este requisito.
Para corregir este comportamiento, debe crear una serie de punteros, regulares o inteligentes (preferiblemente inteligentes para simplificar la administración de la memoria).
Si bien las otras dos respuestas ( here y here ) abordaron el problema desde el punto de vista técnico y explicaron muy bien por qué el compilador tendría una tarea imposible al tratar de compilar el código que le diste, no explicaron el problema conceptual con su pregunta.
El polimorfismo solo puede funcionar cuando estamos hablando de una relación clase-subclase. Así que cuando tengamos la situación como usted ha codificado tenemos:
Estamos diciendo que "Todas las instancias de los Derived
son también instancias de Base
". Tenga en cuenta que esto debe mantenerse o no podemos ni siquiera comenzar a hablar sobre el polimorfismo. En este caso, se mantiene y, por lo tanto, podemos usar el puntero para Derived
donde el código espera un puntero a Base
.
Pero entonces estás intentando hacer algo diferente:
Y aquí tenemos un problema. Mientras que en teoría de conjuntos podríamos decir que un conjunto de una subclase es también un conjunto de una superclase, no es cierto en la programación. El problema aumenta un poco por el hecho de que la diferencia es "solo dos caracteres". Pero el conjunto de elementos es, en cierto modo, algo completamente diferente de cualquiera de esos elementos.
Tal vez si reescribiera el código usando la plantilla std::array
se volvería más claro:
#include <iostream>
#include <array>
struct Base
{
virtual ~Base()
{
std::cout << "~Base()" << std::endl;
}
};
struct Derived : Base
{
~Derived() override
{
std::cout << "~Derived()" << std::endl;
}
};
int main()
{
std::array<Base, 4>* p = new std::array<Derived, 4>;
delete[] p;
}
Este código claramente no puede compilarse, las plantillas no se convierten en subclases unas de otras dependiendo de sus parámetros. En cierto modo, esto sería como esperar que un puntero a una clase se convierta en un puntero a una clase completamente no relacionada, esto simplemente no funcionará.
Esperamos que esto ayude a algunas personas que desean una comprensión más intuitiva de lo que está sucediendo, en lugar de una explicación técnica.
Tiene que ver con los conceptos de covarianza y contravarianza . Daré un ejemplo que espero aclarar las cosas.
Comenzaremos con una jerarquía simple. Supongamos que estamos tratando de crear y destruir objetos en el mundo real. Tenemos objetos 3D (como bloques de madera) y objetos 2D (como hojas de papel). Un objeto 2D puede tratarse como un objeto 3D, excepto con una altura despreciable. Esta es la dirección de subclasificación adecuada, ya que lo contrario no es cierto; Los objetos 2D pueden colocarse uno encima del otro, algo que es muy difícil de hacer con objetos 3D arbitrarios.
Algo que produce objetos, como una impresora, es covariante . Supongamos que su amigo quiere pedir prestada una impresora 3D, tomar diez objetos que produce y pegarlos en una caja. Puedes darles una impresora 2D; Imprimirá diez páginas, y su amigo las pegará en una caja. Sin embargo, si ese amigo quería tomar diez objetos 2D y pegarlos en una carpeta, no se les podía dar una impresora 3D.
Algo que consume objetos, como una trituradora, es contravariante . Tu amigo tiene la carpeta con las páginas impresas, pero no se vendió, por lo que quiere destruirla. Puedes darles una trituradora 3D, como las industriales que se usan para triturar cosas como los coches, y funcionará bien; alimentar las páginas no le causa ninguna dificultad. Por otro lado, si quería destruir la caja de objetos que recibió antes, no podría darle la trituradora 2D, ya que los objetos no caben en la ranura.
Las matrices son invariantes ; ambos consumen objetos (con asignación) y producen objetos (con acceso a matriz). Como ejemplo de este tipo de relación, tome una máquina de fax de algún tipo. Si su amigo quiere una máquina de fax, necesita el tipo exacto que está pidiendo; no pueden tener una super o subclase, ya que no puede pegar objetos 3D en una ranura para papel 2D, y no puede unir objetos producidos por una máquina de fax 3D en un libro.