sencillos programacion poo polimorfismo orientada objetos herencia encapsulamiento ejemplos abstraccion c++ polymorphism

c++ - poo - polimorfismo programacion



¿Por qué no se puede resolver el polimorfismo en tiempo de ejecución en tiempo de compilación? (6)

Considerar:

#include<iostream> using namespace std; class Base { public: virtual void show() { cout<<" In Base /n"; } }; class Derived: public Base { public: void show() { cout<<"In Derived /n"; } }; int main(void) { Base *bp = new Derived; bp->show(); // RUN-TIME POLYMORPHISM return 0; }

¿Por qué este código causa polimorfismo en tiempo de ejecución y por qué no se puede resolver en tiempo de compilación?


¿Por qué este código causa polimorfismo en tiempo de ejecución y por qué no se puede resolver en tiempo de compilación?

¿Qué te hace pensar que sí?

Está haciendo una suposición común: el hecho de que el lenguaje identifique este caso como el uso del polimorfismo en tiempo de ejecución no significa que una implementación tenga que despacharse en tiempo de ejecución. El estándar C ++ tiene una regla llamada "como si": los efectos observables de las reglas del estándar C ++ se describen con respecto a una máquina abstracta, y las implementaciones son libres de lograr dichos efectos observables como lo deseen.

En realidad, la desvirtualización es la palabra general que se usa para hablar sobre las optimizaciones del compilador con el objetivo de resolver llamadas a métodos virtuales en tiempo de compilación.

El objetivo no es tanto reducir la sobrecarga de llamadas virtuales casi imperceptible (si la predicción de bifurcación funciona bien), se trata de eliminar un cuadro negro. Las mejores ganancias, en términos de optimizaciones, se basan en la inclusión de las llamadas: esto abre la propagación constante y una gran cantidad de optimización, y la integración solo se puede lograr cuando se conoce el cuerpo de la función que se llama en tiempo de compilación (ya que implicaba eliminar la llamada y reemplazarla por el cuerpo de la función).

Algunas oportunidades de desvirtualización:

  • una llamada a un método final o un método virtual de una clase final se desvirtualiza trivialmente
  • una llamada a un método virtual de una clase definida en un espacio de nombres anónimo se puede desvirtualizar si esa clase es una hoja en la jerarquía
  • una llamada a un método virtual través de una clase base se puede desvirtualizar si el tipo dinámico del objeto se puede establecer en tiempo de compilación (que es el caso de su ejemplo, con la construcción en la misma función)

Sin embargo, por el estado del arte, querrás leer el blog de Honza Hubička. Honza es un desarrollador de gcc y el año pasado trabajó en la desvirtualización especulativa : el objetivo es calcular las probabilidades de que el tipo dinámico sea A, B o C y luego desvirtualizar especulativamente las llamadas de alguna manera como transformando:

Base& b = ...; b.call();

dentro:

Base& b = ...; if (b.vptr == &VTableOfA) { static_cast<A&>(b).call(); } else if (b.vptr == &VTableOfB) { static_cast<B&>(b).call(); } else if (b.vptr == &VTableOfC) { static_cast<C&>(b).call(); } else { b.call(); } // virtual call as last resort

Honza hizo una publicación de 5 partes:


Básicamente, el compilador debería ser capaz de darse cuenta de que esto no debería resultar en un polimorfismo de tiempo de ejecución en su caso muy simple. Lo más probable es que haya compiladores que realmente lo hacen, pero eso es principalmente una conjetura.

El problema es el caso general cuando en realidad está construyendo un complejo, y aparte de los casos con dependencias de la biblioteca, o la complejidad de analizar las unidades de compilación múltiple posteriores a la compilación, lo que requeriría mantener múltiples versiones del mismo código, lo que eliminaría AST generación , el problema real se reduce a la capacidad de decisión y el problema de detención.

Este último no permite resolver el problema si se puede desvirtualizar una llamada en el caso general.

El problema de detención es decidir si un programa dado una entrada se detendrá ( decimos que el par de entrada de programa se detiene ). Se sabe que no existe un algoritmo general, por ejemplo, un compilador , que resuelva todos los pares posibles de entrada de programa.

Para que el compilador decida para cualquier programa si una llamada debe hacerse virtual o no, debe poder decidir eso para todos los pares posibles de entrada de programa.

Para hacer eso, el compilador necesitaría tener un algoritmo A que decida que dado el programa P1 y el programa P2 donde P2 hace una llamada virtual, luego programe P3 {while ({P1, I}! = {P2, I})} se detiene para cualquier entrada I.

Por lo tanto, el compilador para poder descubrir todas las posibles desvirtualizaciones debe poder decidir eso para cualquier par (P3, I) sobre todos los posibles P3 e I; lo cual es indecidible para todos porque A no existe. Sin embargo, se puede decidir para casos específicos que pueden ser desconcertantes.

Es por eso que en su caso la llamada puede ser descentralizada, pero no en ningún caso.


Eso podría resolverse fácilmente en tiempo de compilación si el optimizador decidiera hacerlo.

El estándar especifica el mismo comportamiento que si hubiera ocurrido el polimorfismo en tiempo de ejecución. No es específico que se logre a través del polimorfismo real en tiempo de ejecución.


Hay muchas razones por las cuales los compiladores en general no pueden reemplazar la decisión de tiempo de ejecución con llamadas estáticas, principalmente porque implica información que no está disponible en el momento de la compilación, por ejemplo, configuración o entrada del usuario. Aparte de eso, quiero señalar dos razones adicionales por las que esto no es posible en general.

Primero, el modelo de compilación de C ++ se basa en unidades de compilación separadas. Cuando se compila una unidad, el compilador solo sabe lo que se define en los archivos fuente que se compilan. Considere una unidad de compilación con una clase base y una función tomada como referencia a la clase base:

struct Base { virtual void polymorphic() = 0; }; void foo(Base& b) {b.polymorphic();}

Cuando se compila por separado, el compilador no tiene conocimiento sobre los tipos que implementan Base y, por lo tanto, no puede eliminar el despacho dinámico. Tampoco es algo que queramos porque queremos poder extender el programa con una nueva funcionalidad mediante la implementación de la interfaz. Puede ser posible hacerlo en el momento del enlace , pero solo bajo el supuesto de que el programa está completamente completo. Las bibliotecas dinámicas pueden romper esta suposición, y como se puede ver a continuación, siempre habrá casos en los que no sea posible.

Una razón más fundamental proviene de la teoría de la computabilidad. Incluso con información completa, no es posible definir un algoritmo que calcule si se alcanzará o no una línea determinada en un programa. Si pudiera, podría resolver el problema de detención: para un programa P , creo un nuevo programa P'' agregando una línea adicional al final de P El algoritmo ahora podría decidir si se alcanza esa línea, lo que resuelve el problema de detención.

No poder decidir en general significa que los compiladores no pueden decidir qué valor se asigna a las variables en general, por ejemplo

bool someFunction( /* arbitrary parameters */ ) { // ... } // ... Base* b = nullptr; if (someFunction( ... )) b = new Derived1(); else b = new Derived2(); b->polymorphicFunction();

Incluso cuando se conocen todos los parámetros en tiempo de compilación, no es posible demostrar en general qué ruta se tomará a través del programa y qué tipo b estático tendrá. Las aproximaciones pueden y se hacen optimizando los compiladores, pero siempre hay casos en los que no funciona.

Dicho esto, los compiladores de C ++ se esfuerzan mucho por eliminar el despacho dinámico porque abre muchas otras posibilidades de optimización, principalmente de poder en línea y propagar el conocimiento a través del código. Si es interesante, puede encontrar una serie de publicaciones de blog interesantes para la implementación de la desvirtualización de GCC .


Los compiladores desvirtualizan rutinariamente tales llamadas, cuando se conoce el tipo estático del objeto. Pegar su código tal como está en el Explorador de compiladores produce el siguiente ensamblado:

main: # @main pushq %rax movl std::cout, %edi movl $.L.str, %esi movl $12, %edx callq std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long) xorl %eax, %eax popq %rdx retq pushq %rax movl std::__ioinit, %edi callq std::ios_base::Init::Init() movl std::ios_base::Init::~Init(), %edi movl std::__ioinit, %esi movl $__dso_handle, %edx popq %rax jmp __cxa_atexit # TAILCALL .L.str: .asciz "In Derived /n"

Incluso si no puede leer el ensamblado, puede ver que solo "In Derived /n" está presente en el ejecutable. No solo se ha optimizado el despacho dinámico, también se ha optimizado toda la clase base.


Porque en el caso general, es imposible en el momento de la compilación determinar qué tipo será en tiempo de ejecución. Su ejemplo se puede resolver en tiempo de compilación (vea la respuesta de @Quentin), pero se pueden construir casos que no pueden, como:

Base *bp; if (rand() % 10 < 5) bp = new Derived; else bp = new Base; bp->show(); // only known at run time

EDITAR: Gracias a @nwp, aquí hay un caso mucho mejor. Algo como:

Base *bp; char c; std::cin >> c; if (c == ''d'') bp = new Derived; else bp = new Base; bp->show(); // only known at run time

Además, por el corolario de la prueba de Turing , se puede demostrar que, en el caso general , es matemáticamente imposible para un compilador de C ++ saber a qué apunta un puntero de clase base en tiempo de ejecución.

Supongamos que tenemos una función similar a un compilador de C ++:

bool bp_points_to_base(const string& program_file);

Eso toma como su entrada program_file : el nombre de cualquier archivo de texto de código fuente C ++ donde un puntero bp (como en el OP) llama a su función miembro virtual show() . Y puede determinar en el caso general (en el punto de secuencia A donde la función miembro virtual show() se invoca primero a través de bp ): si el puntero bp apunta a una instancia de Base o no.

Considere el siguiente fragmento del programa C ++ "q.cpp":

Base *bp; if (bp_points_to_base("q.cpp")) // invokes bp_points_to_base on itself bp = new Derived; else bp = new Base; bp->show(); // sequence point A

Ahora, si bp_points_to_base determina que en "q.cpp": bp apunta a una instancia de Base en A entonces "q.cpp" apunta bp a otra cosa en A Y si determina que en "q.cpp": bp no apunta a una instancia de Base en A , entonces "q.cpp" apunta bp a una instancia de Base en A Esto es una contradicción. Entonces nuestra suposición inicial es incorrecta. Entonces bp_points_to_base no se puede escribir para el caso general .