Recibí esta pregunta cuando recibí un comentario de revisión de código que decía que las funciones virtuales no necesitan estar en línea.
Pensé que las funciones virtuales en línea podrían ser útiles en escenarios donde las funciones se invocan directamente en los objetos. Pero el contraargumento me vino a la mente: ¿por qué querríamos definir lo virtual y luego usar objetos para llamar a los métodos?
¿Es mejor no usar funciones virtuales en línea, ya que casi nunca se expanden de todos modos?
Fragmento de código que utilicé para el análisis:
class Temp
{
public:
virtual ~Temp()
{
}
virtual void myVirtualFunction() const
{
cout<<"Temp::myVirtualFunction"<<endl;
}
};
class TempDerived : public Temp
{
public:
void myVirtualFunction() const
{
cout<<"TempDerived::myVirtualFunction"<<endl;
}
};
int main(void)
{
TempDerived aDerivedObj;
//Compiler thinks it's safe to expand the virtual functions
aDerivedObj.myVirtualFunction();
//type of object Temp points to is always known;
//does compiler still expand virtual functions?
//I doubt compiler would be this much intelligent!
Temp* pTemp = &aDerivedObj;
pTemp->myVirtualFunction();
return 0;
}
pTemp->myVirtualFunction()
podría resolverse como una llamada no virtual, podría tener esa llamada en línea. Esta llamada referenciada está en línea con g ++ 3.4.2:TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();
su código no lo está.Respuestas:
Las funciones virtuales pueden estar en línea a veces. Un extracto de las excelentes preguntas frecuentes de C ++ :
fuente
C ++ 11 ha agregado
final
. Esto cambia la respuesta aceptada: ya no es necesario saber la clase exacta del objeto, es suficiente saber que el objeto tiene al menos el tipo de clase en la que la función se declaró final:fuente
icc
parece hacerlo, según ese enlace.Hay una categoría de funciones virtuales donde todavía tiene sentido tenerlas en línea. Considere el siguiente caso:
La llamada para eliminar 'base', realizará una llamada virtual para llamar al destructor de clase derivado correcto, esta llamada no está en línea. Sin embargo, debido a que cada destructor llama a su destructor padre (que en estos casos está vacío), el compilador puede alinear esas llamadas, ya que no llaman virtualmente a las funciones de la clase base.
El mismo principio existe para los constructores de clases base o para cualquier conjunto de funciones donde la implementación derivada también llama a la implementación de clases base.
fuente
He visto compiladores que no emiten ninguna tabla v si no existe ninguna función no en línea (y se define en un archivo de implementación en lugar de un encabezado). Lanzarían errores como
missing vtable-for-class-A
o algo similar, y estarías confundido como el infierno, como lo estaba yo.De hecho, eso no es conforme con el Estándar, pero sucede, así que considere colocar al menos una función virtual que no esté en el encabezado (si solo es el destructor virtual), de modo que el compilador pueda emitir una vtable para la clase en ese lugar. Sé que sucede con algunas versiones de
gcc
.Como alguien mencionó, las funciones virtuales en línea pueden ser un beneficio a veces , pero, por supuesto, la usará con mayor frecuencia cuando no conozca el tipo dinámico del objeto, porque esa fue la razón principal
virtual
en primer lugar.Sin embargo, el compilador no puede ignorar por completo
inline
. Tiene otra semántica además de acelerar una llamada de función. La línea implícita para las definiciones en clase es el mecanismo que le permite poner la definición en el encabezado: solo lasinline
funciones se pueden definir varias veces en todo el programa sin violar ninguna regla. Al final, se comporta como lo habría definido solo una vez en todo el programa, aunque haya incluido el encabezado varias veces en diferentes archivos vinculados entre sí.fuente
Bueno, en realidad las funciones virtuales siempre pueden estar en línea , siempre que estén unidas estáticamente: supongamos que tenemos una clase abstracta
Base
con una función virtualF
y clases derivadasDerived1
yDerived2
:Una llamada hipotética
b->F();
(conb
tipoBase*
) es obviamente virtual. Pero usted (o el compilador ...) podría reescribirlo así (supongamos quetypeof
es unatypeid
función similar que devuelve un valor que se puede usar en aswitch
)Si bien todavía necesitamos RTTI para
typeof
, la llamada se puede alinear efectivamente, básicamente, incrustando la vtable dentro de la secuencia de instrucciones y especializando la llamada para todas las clases involucradas. Esto también podría generalizarse especializando solo unas pocas clases (por ejemplo, soloDerived1
):fuente
Marcar un método virtual en línea, ayuda a optimizar aún más las funciones virtuales en los siguientes dos casos:
Patrón de plantilla curiosamente recurrente ( http://www.codeproject.com/Tips/537606/Cplusplus-Prefer-Curiously-Recurring-Template-Patt )
Reemplazo de métodos virtuales con plantillas ( http://www.di.unipi.it/~nids/docs/templates_vs_inheritance.html )
fuente
en línea realmente no hace nada, es una pista. El compilador podría ignorarlo o podría incluir un evento de llamada sin incluirlo si ve la implementación y le gusta esta idea. Si la claridad del código está en juego, la línea debe eliminarse.
fuente
Las funciones virtuales declaradas en línea están en línea cuando se llaman a través de objetos y se ignoran cuando se llaman a través de puntero o referencias.
fuente
Con los compiladores modernos, no hará ningún daño incluirlos. Algunos combos antiguos de compiladores / enlazadores podrían haber creado múltiples vtables, pero ya no creo que sea un problema.
fuente
Un compilador solo puede en línea una función cuando la llamada puede resolverse sin ambigüedades en el momento de la compilación.
Sin embargo, las funciones virtuales se resuelven en tiempo de ejecución, por lo que el compilador no puede alinear la llamada, ya que en el tipo de compilación no se puede determinar el tipo dinámico (y, por lo tanto, la implementación de la función a llamar).
fuente
En los casos en que la llamada a la función no es ambigua y la función es un candidato adecuado para la inclusión, el compilador es lo suficientemente inteligente como para incorporar el código de todos modos.
El resto del tiempo "virtual en línea" no tiene sentido, y de hecho algunos compiladores no compilan ese código.
fuente
Tiene sentido crear funciones virtuales y luego llamarlas a objetos en lugar de referencias o punteros. Scott Meyer recomienda, en su libro "c ++ efectivo", nunca redefinir una función no virtual heredada. Eso tiene sentido, porque cuando crea una clase con una función no virtual y redefine la función en una clase derivada, puede estar seguro de usarla usted mismo, pero no puede estar seguro de que otros la usarán correctamente. Además, en una fecha posterior puede usarlo incorrectamente. Por lo tanto, si realiza una función en una clase base y desea que sea redifinable, debe hacerlo virtual. Si tiene sentido realizar funciones virtuales y llamarlas a objetos, también tiene sentido alinearlas.
fuente
En realidad, en algunos casos, agregar "en línea" a una anulación final virtual puede hacer que su código no se compile, ¡así que a veces hay una diferencia (al menos en el compilador VS2017s)!
En realidad, estaba haciendo una función de anulación final virtual en línea en VS2017 agregando el estándar c ++ 17 para compilar y vincular y, por alguna razón, falló cuando utilizo dos proyectos.
Tenía un proyecto de prueba y una DLL de implementación que estoy probando. En el proyecto de prueba, tengo un archivo "linker_includes.cpp" que # incluye los archivos * .cpp del otro proyecto que se necesitan. Lo sé ... sé que puedo configurar msbuild para usar los archivos de objetos de la DLL, pero tenga en cuenta que es una solución específica de Microsoft, mientras que incluir los archivos cpp no está relacionado con el sistema de compilación y es mucho más fácil de versionar un archivo cpp que los archivos xml y la configuración del proyecto y tal ...
Lo interesante es que constantemente recibía un error de vinculador del proyecto de prueba. ¡Incluso si agregué la definición de las funciones que faltan mediante copiar y pegar y no mediante include! Tan raro. El otro proyecto se ha construido y no hay conexión entre los dos que no sea marcar una referencia de proyecto, por lo que hay un orden de construcción para garantizar que ambos siempre se construyan ...
Creo que es algún tipo de error en el compilador. No tengo idea si existe en el compilador enviado con VS2020, porque estoy usando una versión anterior porque algunos SDK solo funcionan con eso correctamente :-(
Solo quería agregar que no solo marcarlos como en línea puede significar algo, sino que incluso podría hacer que su código no se construya en algunas circunstancias raras. Esto es raro, pero bueno saberlo.
PD .: El código en el que estoy trabajando está relacionado con los gráficos de computadora, así que prefiero la inline y es por eso que utilicé tanto final como inline. Mantuve el especificador final para esperar que la versión de lanzamiento sea lo suficientemente inteligente como para compilar el archivo DLL al incluirlo incluso sin que yo lo haya insinuado directamente ...
PS (Linux) .: Espero que no ocurra lo mismo en gcc o clang como solía hacer este tipo de cosas de forma rutinaria. No estoy seguro de dónde viene este problema ... prefiero hacer c ++ en Linux o al menos con algunos gcc, pero a veces el proyecto es diferente en necesidades.
fuente