friends - extend class c++
¿Por qué el polimorfismo no funciona sin punteros/referencias? (6)
Ya encontré algunas preguntas en SO con título similar, pero cuando leí las respuestas se centraron en diferentes partes de la pregunta que eran realmente específicas (por ejemplo, STL / contenedores).
¿Podría alguien mostrarme por qué debe usar punteros / referencias para implementar el polimorfismo? Puedo entender que los indicadores pueden ayudar, pero seguramente las referencias solo diferencian entre el valor de paso y el de paso por referencia.
Seguramente siempre que asignes memoria en el montón, para que puedas tener un enlace dinámico, esto hubiera sido suficiente, obviamente no.
"Seguramente siempre que asignes memoria en el montón": donde se asigna la memoria no tiene nada que ver con eso. Se trata de la semántica. Tome, por ejemplo:
Derived d;
Base* b = &d;
d
está en la pila (memoria automática), pero el polimorfismo seguirá funcionando en b
.
Si no tiene un puntero de clase base o referencia a una clase derivada, el polimorfismo no funciona porque ya no tiene una clase derivada. Tomar
Base c = Derived();
El objeto c
no es un Derived
, sino una Base
, debido a la división . Entonces, técnicamente, el polimorfismo aún funciona, es solo que ya no tiene un objeto Derived
para hablar.
Ahora toma
Base* c = new Derived();
c
solo señala algún lugar en la memoria, y realmente no le importa si eso es en realidad una Base
o un Derived
, pero la llamada a un método virtual
se resolverá dinámicamente.
Considere las pequeñas arquitecturas endian: los valores se almacenan en bytes de orden inferior primero. Entonces, para cualquier entero sin signo dado, los valores 0-255 se almacenan en el primer byte del valor. El acceso a los 8 bits bajos de cualquier valor simplemente requiere un puntero a su dirección.
Entonces podríamos implementar uint8
como clase. Sabemos que una instancia de uint8
es ... un byte. Si derivamos de él y producimos uint16
, uint32
, etc., la interfaz sigue siendo la misma a los fines de la abstracción, pero el cambio más importante es el tamaño de las instancias concretas del objeto.
Por supuesto, si implementamos uint8
y char
, los tamaños pueden ser los mismos, del mismo modo sint8
.
Sin embargo, operator=
of uint8
y uint16
van a mover diferentes cantidades de datos.
Para crear una función polimórfica, debemos ser capaces de:
a / recibir el argumento por valor copiando los datos en una nueva ubicación del tamaño y diseño correctos, b / tomar un puntero a la ubicación del objeto, c / tomar una referencia a la instancia del objeto,
Podemos usar plantillas para lograr a, por lo que el polimorfismo puede funcionar sin punteros y referencias, pero si no estamos contando plantillas, entonces consideremos qué sucede si implementamos uint128
y lo pasamos a una función que espera uint8
. Respuesta: 8 bits se copian en lugar de 128.
Entonces, ¿qué pasa si hacemos que nuestra función polimórfica acepte uint128
y la uint128
uint8
? Si nuestro uint8
estábamos copiando desafortunadamente fue localizado, nuestra función intentaría copiar 128 bytes de los cuales 127 estaban fuera de nuestra memoria accesible -> bloqueo.
Considera lo siguiente:
class A { int x; };
A fn(A a)
{
return a;
}
class B : public A {
uint64_t a, b, c;
B(int x_, uint64_t a_, uint64_t b_, uint64_t c_)
: A(x_), a(a_), b(b_), c(c_) {}
};
B b1 { 10, 1, 2, 3 };
B b2 = fn(b1);
// b2.x == 10, but a, b and c?
En el momento en que se compiló fn
, no había conocimiento de B
Sin embargo, B
se deriva de A
para que el polimorfismo permita que podamos llamar a fn
con B
Sin embargo, el objeto que devuelve debe ser una A
comprenda un solo int.
Si pasamos una instancia de B
a esta función, lo que recuperamos debería ser solo un { int x; }
{ int x; }
sin a, b, c.
Esto es "rebanar".
Incluso con punteros y referencias, no evitamos esto de forma gratuita. Considerar:
std::vector<A*> vec;
Los elementos de este vector podrían ser indicadores de A
o algo derivado de A
El lenguaje generalmente resuelve esto mediante el uso de "vtable", una pequeña adición a la instancia del objeto que identifica el tipo y proporciona punteros a funciones para funciones virtuales. Puedes pensar que es algo así como:
template<class T>
struct PolymorphicObject {
T::vtable* __vtptr;
T __instance;
};
En lugar de que cada objeto tenga su propia variable vtable, las clases las tienen, y las instancias de objeto simplemente apuntan a la tabla de contenido relevante.
El problema ahora no es cortar, sino corregir el tipo:
struct A { virtual const char* fn() { return "A"; } };
struct B : public A { virtual const char* fn() { return "B"; } };
#include <iostream>
#include <cstring>
int main()
{
A* a = new A();
B* b = new B();
memcpy(a, b, sizeof(A));
std::cout << "sizeof A = " << sizeof(A)
<< " a->fn(): " << a->fn() << ''/n'';
}
sizeof A = 4 a->fn(): B
Lo que deberíamos haber hecho es usar a->operator=(b)
pero, de nuevo, esto es copiar una A a una A y rebanar así ocurriría:
struct A { int i; A(int i_) : i(i_) {} virtual const char* fn() { return "A"; } };
struct B : public A {
int j;
B(int i_) : A(i_), j(i_ + 10) {}
virtual const char* fn() { return "B"; }
};
#include <iostream>
#include <cstring>
int main()
{
A* a = new A(1);
B* b = new B(2);
*a = *b; // aka a->operator=(static_cast<A*>(*b));
std::cout << "sizeof A = " << sizeof(A)
<< ", a->i = " << a->i << ", a->fn(): " << a->fn() << ''/n'';
}
( i
se copia, pero se pierde B j
)
La conclusión aquí es que se requieren punteros / referencias porque la instancia original lleva consigo información de pertenencia con la que la copia puede interactuar.
Pero también, ese polimorfismo no está perfectamente resuelto dentro de C ++ y uno debe ser consciente de su obligación de proporcionar / bloquear acciones que podrían producir cortes.
Cuando un objeto se pasa por valor, generalmente se coloca en la pila. Poner algo en la pila requiere conocimiento de cuán grande es. Al usar el polimorfismo, usted sabe que el objeto entrante implementa un conjunto particular de características, pero generalmente no tiene idea del tamaño del objeto (ni usted, necesariamente, es parte del beneficio). Por lo tanto, no puedes ponerlo en la pila. Sin embargo, siempre se sabe el tamaño de un puntero.
Ahora, no todo va en la pila, y hay otras circunstancias atenuantes. En el caso de los métodos virtuales, el puntero al objeto también es un puntero a las tablas vtable (s) del objeto, que indican dónde están los métodos. Esto permite que el compilador encuentre y llame a las funciones, independientemente del objeto con el que esté trabajando.
Otra causa es que muy a menudo el objeto se implementa fuera de la biblioteca de llamadas y se asigna con un administrador de memoria completamente diferente (y posiblemente incompatible). También podría tener miembros que no se pueden copiar, o podría causar problemas si se copiaran con un administrador diferente. Podría haber efectos secundarios a la copia y todo tipo de otras complicaciones.
El resultado es que el puntero es la única información sobre el objeto que usted realmente entiende y proporciona suficiente información para averiguar dónde están los otros bits que necesita.
En C ++, un objeto siempre tiene un tipo y tamaño fijo conocido en tiempo de compilación y (si puede y tiene su dirección tomada) siempre existe en una dirección fija para la duración de su duración. Estas son características heredadas de C que ayudan a que ambos lenguajes sean adecuados para la programación de sistemas de bajo nivel. (Todo esto está sujeto a la regla de si ... si: un compilador conforme es libre de hacer lo que le plazca con el código, siempre que pueda demostrarse que no tiene un efecto detectable en el comportamiento de un programa conforme que esté garantizado). por el estándar.)
Una función virtual
en C ++ se define (más o menos, sin necesidad de abogacía de lenguaje extremo) como la ejecución basada en el tipo de tiempo de ejecución de un objeto; cuando se llama directamente a un objeto, este siempre será el tipo de tiempo de compilación del objeto, por lo que no hay polimorfismo cuando se llama a una función virtual
esta manera.
Tenga en cuenta que esto no necesariamente tiene que ser el caso: los tipos de objetos con funciones virtual
generalmente se implementan en C ++ con un puntero por objeto a una tabla de funciones virtual
que es única para cada tipo. Si así lo desea, un compilador de alguna variante hipotética de C ++ podría implementar la asignación en objetos (como Base b; b = Derived()
) como copiar tanto el contenido del objeto como el puntero de la tabla virtual
, lo que funcionaría fácilmente. si tanto la Base
como la Derived
tienen el mismo tamaño. En el caso de que los dos no tuvieran el mismo tamaño, el compilador podría incluso insertar código que pause el programa por una cantidad arbitraria de tiempo para reorganizar la memoria en el programa y actualizar todas las referencias posibles a esa memoria de una manera que podría ser demostrado que no tiene ningún efecto detectable en la semántica del programa, terminando el programa si no se puede encontrar dicha reorganización: sin embargo, esto sería muy ineficiente, y no se puede garantizar que se detenga, características obviamente no deseables para un operador de asignación a tener.
Por lo tanto, en lugar de lo anterior, el polimorfismo en C ++ se logra al permitir referencias y punteros a objetos para referenciar y apuntar a objetos de sus tipos declarados en tiempo de compilación y cualquier subtipo de los mismos. Cuando se llama a una función virtual
través de una referencia o puntero, y el compilador no puede probar que el objeto al que se hace referencia o apunta es de un tipo de tiempo de ejecución con una implementación conocida específica de esa función virtual
, el compilador inserta el código que busca el correcto función virtual
para llamar a un tiempo de ejecución. Tampoco tenía que ser así: las referencias y los indicadores podrían haberse definido como no polimórficos (no permitiéndoles referenciar o apuntar a subtipos de sus tipos declarados) y forzar al programador a encontrar formas alternativas de implementar el polimorfismo. . Esto último es claramente posible ya que está hecho todo el tiempo en C, pero en ese momento no hay muchas razones para tener un nuevo idioma.
En resumen, la semántica de C ++ está diseñada de tal manera que permite la abstracción de alto nivel y el encapsulado del polimorfismo orientado a objetos al tiempo que conserva características (como el acceso de bajo nivel y la gestión explícita de la memoria) que le permiten ser adecuado para desarrollo de bajo nivel. Podrías diseñar fácilmente un lenguaje que tuviera alguna otra semántica, pero no sería C ++ y tendría diferentes beneficios e inconvenientes.
Me pareció realmente útil comprender que se invoca un constructor de copias cuando se asigna de esta manera:
class Base { };
class Derived : public Base { };
Derived x; /* Derived type object created */
Base y = x; /* Copy is made (using Base''s copy constructor), so y really is of type Base. Copy can cause "slicing" btw. */
Como y es un objeto real de la clase Base, en lugar del original, las funciones a las que se llama son las funciones de Base.
Necesita punteros o referencia porque para el tipo de polimorfismo que le interesa (*), necesita que el tipo dinámico sea diferente del tipo estático, en otras palabras, que el tipo verdadero del objeto sea diferente del tipo declarado. En C ++ eso ocurre solo con punteros o referencias.
(*) Genericity, el tipo de polimorfismo proporcionado por las plantillas, no necesita punteros ni referencias.