virtuales tipos tiempo qué puras polimorfismo herencia funciones ejecucion conoce como clases clase abstractas abstracta c++ standards pure-virtual

c++ - tiempo - tipos de polimorfismo en java



¿Por qué una llamada virtual a una función virtual pura de un constructor es UB y el estándar permite una llamada a una función virtual no pura? (4)

Antes de discutir por qué no está definido, primero aclaremos de qué trata la pregunta.

#include<iostream> using namespace std; struct Abstract { virtual void pure() = 0; virtual void impure() { cout << " Abstract :: impure() " << endl; } Abstract() { impure(); // pure(); // would be undefined } ~Abstract() { impure(); // pure(); // would be undefined } }; struct X : public Abstract { virtual void pure() { cout << " X :: pure() " << endl; } virtual void impure() { cout << " X :: impure() " << endl; } }; int main() { X x; x.pure(); x.impure(); }

La salida de esto es:

Abstract :: impure() // called while x is being constructed X :: pure() // x.pure(); X :: impure() // x.impure(); Abstract :: impure() // called while x is being destructed.

La segunda y tercera líneas son fáciles de entender; los métodos se definieron originalmente en Resumen, pero las modificaciones en X se hacen cargo. Este resultado habría sido el mismo incluso si x hubiera sido una referencia o un puntero de tipo Abstracto en lugar de X.

Pero esta cosa interesante es lo que ocurre dentro del constructor y el destructor de X. La llamada a impure() en el constructor se llama Abstract::impure() , no X::impure() , aunque el objeto que se está construyendo es de tipo X . Lo mismo sucede en el destructor.

Cuando se construye un objeto de tipo X , lo primero que se construye es simplemente un objeto Abstract y, de manera crucial, ignora el hecho de que, en última instancia, será un objeto X El mismo proceso ocurre a la inversa para la destrucción.

Ahora, suponiendo que lo entiendas, está claro por qué el comportamiento debe ser indefinido. No hay un método Abstract :: pure que pueda ser llamado por el constructor o el destructor, y por lo tanto no sería significativo tratar de definir este comportamiento (excepto posiblemente como un error de compilación).

Actualización: Acabo de discovered que es posible dar una implementación, en la clase virtual , de un método virtual puro. La pregunta es: ¿es esto significativo?

struct Abstract { virtual void pure() = 0; }; void Abstract :: pure() { cout << "How can I be called?!" << endl; }

Nunca habrá un objeto cuyo tipo dinámico sea Abstracto, por lo tanto, nunca podrá ejecutar este código con una llamada normal a abs.pure(); o algo por el estilo. Entonces, ¿cuál es el punto de permitir tal definición?

Vea esta demostración . El compilador emite advertencias, pero ahora el método Abstract::pure() se puede llamar desde el constructor. Esta es la única ruta por la cual se puede llamar a Abstract::pure() .

Pero , esto es técnicamente indefinido. Otro compilador tiene derecho a ignorar la implementación de Abstract::pure , o incluso a hacer otras locuras. No sé por qué no está definido, pero escribí esto para intentar ayudar a aclarar la pregunta.

A partir de las 10.4 clases abstractas parag. 6 en la Norma:

"Las funciones miembro se pueden llamar desde un constructor (o destructor) de una clase abstracta; el efecto de hacer una llamada virtual a una función virtual pura directa o indirectamente para el objeto que se crea (o destruye) desde dicho constructor (o destructor) es indefinido."

Suponiendo que el Estándar permite una llamada a una función virtual no pura desde un constructor (o destructor), ¿por qué la diferencia?

[EDITAR] Más citas de estándares sobre funciones virtuales puras:

§ 10.4 / 2 Una función virtual se especifica pura usando un especificador puro (9.2) en la declaración de la función en la definición de clase. Una función virtual pura debe definirse solo si se llama con, o como con (12.4), la sintaxis de id-calificado (5.1). ... [Nota: una declaración de función no puede proporcionar un especificador puro y una definición — nota final]

§ 12.4 / 9 Un destructor puede ser declarado virtual (10.3) o puro virtual (10.4); Si se crea algún objeto de esa clase o cualquier clase derivada en el programa, se definirá el destructor.

Algunas preguntas que necesitan respuesta son:

  • Cuando a la función virtual pura no se le ha dado una implementación, ¿no debería esto ser un error del compilador o del vinculador?

  • Cuando a la función virtual pura se le ha dado una implementación, ¿por qué no puede estar bien definida en este caso invocar esta función?


Creo que este código es un ejemplo del comportamiento indefinido al que hace referencia el estándar. En particular, no es fácil para el compilador darse cuenta de que esto no está definido.

(Por cierto, cuando digo "compilador", me refiero a "compilador y vinculador". Disculpas por cualquier confusión).

struct Abstract { virtual void pure() = 0; virtual void foo() { pure(); } Abstract() { foo(); } ~Abstract() { foo(); } }; struct X : public Abstract { virtual void pure() { cout << " X :: pure() " << endl; } virtual void impure() { cout << " X :: impure() " << endl; } }; int main() { X x; }

Si el constructor de Abstract directamente llamado pure() , esto obviamente sería un problema y un compilador puede ver fácilmente que no hay Abstract::pure() para ser llamado, y g ++ da una advertencia. Pero en este ejemplo, el constructor llama a foo() , y foo() es una función virtual pura. Por lo tanto, no existe una base directa para que el compilador o el vinculador emita una advertencia o error.

Como espectadores, podemos ver que foo es un problema si se llama desde el constructor de Abstract. Abstract::foo() sí está definido, pero intenta llamar a Abstract::pure y esto no existe.

En esta etapa, podría pensar que el compilador debería emitir una advertencia / error sobre foo por el hecho de que llama a una función virtual pura. Pero en su lugar, debe considerar la clase no abstracta derivada donde pure se le ha dado una implementación. Si llama a foo en esa clase después de la construcción (y suponiendo que no la ha foo ), entonces obtendrá un comportamiento bien definido. Así que de nuevo, no hay base para una advertencia sobre foo. foo está bien definido siempre y cuando no se llame en el constructor de Abstract .

Por lo tanto, cada método (el constructor y foo) están relativamente bien si los miras por su cuenta. La única razón por la que sabemos que hay un problema es porque podemos ver el panorama general. Un compilador muy inteligente pondría cada implementación / no implementación particular en una de tres categorías:

  • Totalmente definido: es, y todos los métodos que llama están completamente definidos en cada nivel en la jerarquía de objetos
  • Definido después de la construcción. Una función como foo que tiene una implementación pero que puede ser contraproducente dependiendo del estado de los métodos a los que llama.
  • Puro virtual.

Es mucho trabajo esperar que un compilador y un enlazador hagan un seguimiento de todo esto, y por lo tanto, el estándar permite que los compiladores lo compilen limpiamente pero que den un comportamiento indefinido.

(No he mencionado el hecho de que es posible dar implementaciones a métodos puramente virtuales. Esto es nuevo para mí. ¿Está definido correctamente o es solo una extensión específica del compilador? void Abstract :: pure() { } )

Entonces, no es simplemente indefinido "porque la norma lo dice". Debe preguntarse ''¿qué comportamiento definiría para el código anterior?''. La única respuesta sensata es dejarlo indefinido o imponer un error en tiempo de ejecución. Al compilador y al enlazador no les resultará fácil analizar todas estas dependencias.

Y para empeorar las cosas, ¡considere las funciones de punteros a miembros! El compilador o el enlazador realmente no puede decir si los métodos "problemáticos" serán llamados alguna vez, podría depender de una carga completa de otras cosas que suceden en tiempo de ejecución. Si el compilador ve (this->*mem_fun)() en el constructor, no se puede esperar que sepa qué tan bien definido está mem_fun .


Debido a que una llamada virtual NUNCA puede llamar a una función virtual pura, la única manera de llamar a una función virtual pura es con una llamada explícita (calificada).

Ahora, fuera de los constructores o destructores, esto se debe al hecho de que nunca se pueden tener objetos de una clase abstracta. En su lugar, debe tener un objeto de alguna clase derivada no abstracta que anule la función virtual pura (si no la anulara, la clase sería abstracta). Sin embargo, mientras se está ejecutando un constructor o destructor, es posible que tenga un objeto de estado intermedio. Pero dado que el estándar dice que intentar llamar a una función virtual pura virtualmente en este estado da como resultado un comportamiento indefinido, el compilador es libre de no tener que tener en cuenta las cosas del caso especial para hacerlo bien, dando mucha más flexibilidad para implementar funciones virtuales puras. En particular, el compilador es libre de implementar los virtuales puros de la misma manera que implementa los virtuales no puros (no se necesita un caso especial), y falla o falla de otra manera si llama al virtual puro desde un ctor / dtor.


Es la forma en que se construyen y destruyen las clases.

La base se construye primero, luego se deriva. Así que en el constructor de Base, Derived aún no ha sido creado. Por lo tanto, ninguna de sus funciones miembro puede ser llamada. Entonces, si el constructor de Base llama a una función virtual, no puede ser la implementación de Derived, debe ser la de Base. Pero la función en Base es virtual y no hay nada a lo que llamar.

En la destrucción, primero se deriva Derivado, luego Base. Así que, una vez más, en el destructor de Base no hay ningún objeto Derivado para invocar la función, solo Base.

Por cierto, solo está indefinido cuando la función sigue siendo pura virtual. Así que esto está bien definido:

struct Base { virtual ~Base() { /* calling foo here would be undefined */} virtual void foo() = 0; }; struct Derived : public Base { ~Derived() { foo(); } virtual void foo() { } };

La discusión ha seguido sugiriendo alternativas que:

  • Puede producir un error de compilación, al igual que intentar crear una instancia de una clase abstracta.

El código de ejemplo sería sin duda algo como: class Base {// other stuff virtual void init () = 0; limpieza de vacío virtual () = 0; };

Base::Base() { init(); // pure virtual function } Base::~Base() { cleanup(); // which is a pure virtual function. You can''t do that! shouts the compiler. }

Aquí está claro que lo que estás haciendo te va a meter en problemas. Un buen compilador podría emitir una advertencia.

  • podría producir un error de enlace

La alternativa es buscar una definición de Base::init() y Base::cleanup() e invocarla si existe, de lo contrario invocar un error de enlace, es decir, tratar la limpieza como no virtual con el propósito de constructores y destructores.

El problema es que no funcionará si tiene una función no virtual que llama a la función virtual.

class Base { void init(); void cleanup(); // other stuff. Assume access given as appropriate in examples virtual ~Base(); virtual void doinit() = 0; virtual void docleanup() = 0; }; Base::Base() { init(); // non-virtual function } Base::~Base() { cleanup(); } void Base::init() { doinit(); } void Base::cleanup() { docleanup(); }

Esta situación me parece que está más allá de la capacidad tanto del compilador como del enlazador. Recuerda que estas definiciones podrían estar en cualquier unidad de compilación. No hay nada ilegal en que el constructor y el destructor llamen a init () o cleanup () a menos que sepa lo que van a hacer, y no hay nada ilegal en que init () y cleanup () llamen a las funciones virtuales puras a menos que sepa de donde se invocan.

Es totalmente imposible para el compilador o enlazador hacer esto.

Por lo tanto, el estándar debe permitir la compilación y el enlace y marcarlo como "comportamiento indefinido".

Por supuesto, si existe una implementación, el compilador es libre de usarla si puede. El comportamiento indefinido no significa que tiene que bloquearse. Solo que el estándar no dice que tiene que usarlo.

Tenga en cuenta que en este caso, el destructor está llamando a una función miembro que llama a virtual puro, pero ¿cómo sabe que hará esto incluso? Podría estar llamando a algo en una biblioteca completamente diferente que invoca la función virtual pura (asumiendo que el acceso está ahí).

Base::~Base() { someCollection.removeMe( this ); } void CollectionType::removeMe( Base* base ) { base->cleanup(); // ouch }

Si CollectionType existe en una biblioteca totalmente diferente, no hay forma de que ocurra un error de enlace aquí. Lo simple es que la combinación de estas llamadas es mala (pero ninguna de las dos está defectuosa). Si removeMe va a llamar puramente limpieza virtual (), no se puede llamar desde el destructor de Base, y viceversa.

Una última cosa que debes recordar acerca de Base::init() y Base::cleanup() aquí es que, incluso si tienen implementaciones, nunca se llaman a través del mecanismo de función virtual (v-table). Solo se les llamaría explícitamente (utilizando la calificación completa de nombre de clase), lo que significa que en realidad no son realmente virtuales. El hecho de que se les permita dar implementaciones puede ser engañoso, probablemente no fue realmente una buena idea y si deseaba una función de este tipo que pudiera llamarse a través de clases derivadas, tal vez sea mejor estar protegido y no ser virtual.

Esencialmente: si desea que la función tenga el comportamiento de una función virtual no pura, de modo que le asigne una implementación y sea llamada en la fase de constructor y destructor, no la defina como virtual. ¿Por qué definirlo como algo que no quieres que sea?

Si todo lo que quiere hacer es evitar que se creen instancias, puede hacerlo de otras maneras, como: - Hacer que el destructor sea simplemente virtual. - Hacer los constructores todos protegidos.