virtuales que puro puras polimorfismo objeto modificador herencia funciones c++ c++14 auto virtual-functions vtable

c++ - que - ¿Por qué las funciones virtuales no pueden usar la deducción del tipo de retorno?



polimorfismo puro c++ (3)

Bueno, el tipo de retorno deducido de la función solo se conoce en el punto de definición de la función: el tipo de retorno se deduce de las declaraciones de return dentro del cuerpo de la función.

Mientras tanto, vtable se crea y la anulación de la semántica se verifica basándose únicamente en las declaraciones de funciones presentes en la definición de clase. Estas verificaciones nunca se basaron en la definición de la función y nunca se necesitaron para ver la definición. Por ejemplo, el idioma requiere que la función de reemplazo tenga el mismo tipo de retorno o un tipo de retorno covariante que la función que reemplaza. Cuando la declaración de función no definitoria especifica un tipo de retorno deducido (es decir, auto sin tipo de retorno final), su tipo de retorno es desconocido en ese punto y permanece desconocido hasta que el compilador encuentre la definición de la función. No es posible realizar la verificación del tipo de retorno antes mencionada cuando se desconoce el tipo de retorno. Pedirle al compilador que posponga de alguna manera la verificación del tipo de retorno hasta el punto en que se haga conocido requeriría un rediseño cualitativo importante de esta área fundamental de la especificación del lenguaje. (No estoy seguro de que sea posible).

Otra alternativa sería liberar al compilador de esa carga bajo el mandato general de "no se requiere diagnóstico" o "el comportamiento no está definido", es decir, entregar la responsabilidad al usuario, pero eso también constituiría una desviación importante de la primera. Diseño del lenguaje.

Básicamente, por una razón similar no puede aplicar el operador & a una función declarada como auto f(); pero aún no está definido, como lo muestra el ejemplo en 7.1.6.3/11.

n3797 dice:

§ 7.1.6.4/14:

Una función declarada con un tipo de retorno que utiliza un tipo de marcador de posición no será virtual (10.3).

Por lo tanto el siguiente programa está mal formado:

struct s { virtual auto foo() { } };

Todo lo que puedo encontrar para la justificación es este vago de n3638 :

virtual

Sería posible permitir la deducción de tipos de retorno para funciones virtuales, pero eso complicaría tanto la verificación de anulación como el diseño de vtable, por lo que parece preferible prohibir esto.

¿Alguien puede proporcionar más razones o dar un buen ejemplo (código) que esté de acuerdo con la cita anterior?


El razonamiento que incluyó es razonablemente claro: naturalmente, las subclases deben anular las funciones virtuales, de modo que usted, como diseñador de la clase base, debería hacer que las personas que heredan su clase sean lo más fáciles posible para proporcionar una anulación adecuada. Sin embargo, si usa auto , descifrar el tipo de retorno para la anulación se convierte en una tarea tediosa para un programador. Los compiladores tendrían menos problemas con esto, pero los humanos tendrían muchas oportunidades para confundirse.

Por ejemplo, si ve una declaración de retorno que se parece a esto

return a * 3 + b;

tendría que rastrear el programa hasta el punto de declaración de a y b , averiguar el tipo de promociones y decidir cuál será el tipo de devolución.

Parece que los diseñadores de idiomas se dieron cuenta de que esto sería bastante confuso, y decidieron no permitir esta característica.


auto es un tipo desconocido en una ecuación de tipo; Como es habitual, el tipo debe definirse en algún punto. Una función virtual necesita tener una definición, siempre se "usa" incluso si la función nunca se llama en el programa.

Breve descripción del tema vtable

Los tipos de devoluciones covariantes son un problema de implementación con vtable: las devoluciones covariantes son una característica internamente poderosa (luego castradas por reglas de lenguaje arbitrarias). La covarianza se limita a los punteros (y las referencias) derivados de las conversiones básicas, pero el poder interno y, por lo tanto, la dificultad de implementación es casi el de las conversiones arbitrarias: derivadas de la base al código arbitrario (derivadas de la base restringidas a los sub-objetos exclusivos de la clase base, también conocidos como herencia no virtual, sería mucho más simple).

La covarianza en el caso de la conversión a subobjetos base compartidos (también conocida como herencia virtual) significa que la conversión no solo puede cambiar la representación del valor del puntero, sino que también cambia su valor de una manera que pierde información, en el caso general.

Por lo tanto, la covarianza virtual (tipo de retorno covariante que implica la conversión de herencia virtual) significa que el anulador no se puede confundir con la función anulada en una situación de base primaria.

Explicación detallada

Teoría básica de vtables y bases primarias.

struct Primbase { virtual void foo(); // new }; struct Der : Primbase { // primary base void foo(); // replace Primbase::foo() virtual void bar(); // new slot };

Primbase es la base principal aquí, comienza en la misma dirección en el objeto derivado. Esto es extremadamente importante: para la base principal, las conversiones hacia arriba / hacia abajo se pueden realizar con una reinterpretación o conversión de estilo C en el código generado. La herencia única es mucho más fácil para el implementador porque solo hay clases base primarias. Con herencia múltiple, se necesita aritmética de punteros.

Solo hay un vptr en Der , el de Primbase ; hay un vtable para Der , diseño compatible con el vtable de Primbase .

En este caso, el compilador habitual no asignará otra ranura para Der::foo() en vtable, ya que la función derivada se llama (en forma hipotética el código C generado) con un Primbase* a Primbase* , no a Der* . El Der vtable solo tiene dos ranuras (más los datos RTTI).

Covarianza primaria

Ahora agregamos un poco de covarianza simple:

struct Primbase { virtual Primbase *foo(); // new slot in vtable }; struct Der : Primbase { // primary base Der *foo(); // replaces Primbase::foo() in vtable virtual void bar(); // new slot };

Aquí la covarianza es trivial, ya que implica una base primaria. Nada que ver a nivel de código compilado.

Covarianza de compensación no cero

Mas complejo:

struct Basebelow { virtual void bar(); // new slot }; struct Primbase { virtual Basebelow *foo(); // new }; struct Der : Primbase, // primary base Basebelow { // base at a non zero offset Der *foo(); // new slot? };

En este caso, la representación de un Der* no es la misma que la representación de su clase de base. Dos opciones de implementación:

  • (establecer) establecer en la interfaz de llamada virtual Basebelow *(Primbase::foo)() para toda la jerarquía: this es un Primbase* (compatible con Der* ) pero el tipo de valor de retorno no es compatible (representación diferente), por lo que la implementación de la función convertirá el Der* en un Primbase* (aritmética de punteros) y la persona que llama convertirá de nuevo cuando realice una llamada virtual en un Der ;

  • (introduzca) otra ranura de función virtual en Der vtable para la función que devuelve un Der* .

Generalizado en una jerarquía de compartir: covarianza virtual

En el caso general, los subobjetos de la clase base son compartidos por diferentes clases derivadas, esto es virtual "diamante":

struct B {}; struct L : virtual B {}; struct R : virtual B {}; struct D : L, R {};

Aquí, la conversión a B* es dinámica, basada en el tipo de tiempo de ejecución (a menudo utilizando vptr, o bien punteros / desplazamientos internos en los objetos, como en MSVC).

En general, tales conversiones a subobjetos de clase base pierden información y no se pueden deshacer. No hay conversión de B* a L* confiable. Por lo tanto, la opción (liquidar) no está disponible. La implementación tendrá que (introducir) .

Ejemplo: Vtable para una anulación con un tipo de retorno covariante en el ABI de Itanium

El Itanium C ++ ABI describe el diseño de la vtable . Aquí está la regla con respecto a la introducción de las entradas de vtable para una clase derivada (en particular una con una clase base primaria):

Hay una entrada para cualquier función virtual declarada en una clase, ya sea una nueva función o invalide una función de clase base, a menos que anule una función de la base primaria, y la conversión entre sus tipos de retorno no requiere un ajuste .

(énfasis mío)

Entonces, cuando una función invalida una declaración en la clase base, se compara el tipo de retorno: si son similares, es decir, una es invariablemente una clase base primaria de la otra, en otras palabras, siempre en el desplazamiento 0, no hay ninguna entrada vtable adicional.

Volver a auto emisión auto

(introducción) no es una elección de implementación complicada, pero hace que vtable crezca: el diseño de vtable está determinado por la cantidad de (introducción) realizada.

Por lo tanto, el diseño de vtable está determinado por el número de funciones virtuales (que conocemos de la definición de clase), la presencia de funciones virtuales covariantes (que solo podemos conocer de los tipos de retorno de funciones) y el tipo de covarianza : covarianza primaria, no -Cero desviación de la covarianza o covarianza virtual.

Conclusión

El diseño del vtable solo se puede determinar sabiendo el tipo de retorno de los anuladores virtuales de las funciones virtuales de la clase base devolviendo un puntero (o referencia) a un tipo de clase . El cálculo de vtable tendría que retrasarse cuando hay tales anulantes en una clase.

Esto complicaría la implementación.

Nota: los términos como "covarianza virtual" utilizados están todos inventados, excepto "base primaria" que se define oficialmente en el ABI de Itanium C ++.

EDIT: ¿Por qué creo que la comprobación de restricciones no es un problema

La verificación de las restricciones covariantes no es un problema, no rompe la compilación separada o el modelo de C ++:

anulador auto de una función de retorno de puntero de clase (/ ref) puntero

struct B { virtual int f(); virtual B *g(); }; struct D : B { auto f(); // int f() auto g(); // ? };

El tipo de f() está totalmente restringido y la definición de la función debe devolver un int .

El tipo de retorno de g() está parcialmente restringido: puede ser B* o algún derived_from_B* . La comprobación se producirá en el punto de definición.

Anulación de una función virtual automática

Considere una clase derivada potencial D2 :

struct D2 : D { T1 f(); // T1 must be int T2 g(); // ? };

Aquí se pueden verificar las restricciones en f() , ya que T1 debe ser int , pero no las restricciones en T2 , porque la declaración de D::g() no se conoce. Todo lo que sabemos es que T2 debe ser un puntero a una subclase de B (posiblemente solo B ).

La definición de D::g() puede ser covariante e introducir una restricción más fuerte:

auto D::g() { return new D; } // covariant D* return

por lo que T2 debe ser un puntero a una clase derivada de D (posiblemente solo D ).

Antes de ver la definición, no podemos conocer esta restricción.

Debido a que la declaración de anulación no se puede verificar antes de ver la definición, se debe rechazar .

Por simplicidad, creo que f() también debería ser rechazada.