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 unPrimbase*
(compatible conDer*
) pero el tipo de valor de retorno no es compatible (representación diferente), por lo que la implementación de la función convertirá elDer*
en unPrimbase*
(aritmética de punteros) y la persona que llama convertirá de nuevo cuando realice una llamada virtual en unDer
;(introduzca) otra ranura de función virtual en
Der
vtable para la función que devuelve unDer*
.
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.