c++ - una - ¿Las funciones virtuales en línea realmente no tienen sentido?
que es un objeto en c++ (12)
Recibí esta pregunta cuando recibí un comentario de revisión del 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 son llamadas directamente a los objetos. Pero el contraargumento que me vino a la mente es: ¿por qué querría uno definir virtuales y luego usar objetos para llamar a 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;
}
Bueno, en realidad las funciones virtuales siempre pueden estar en línea , siempre que estén vinculadas estáticamente entre sí: supongamos que tenemos una clase abstracta Base
con una función virtual F
y clases derivadas Derived1
y Derived2
:
class Base {
virtual void F() = 0;
};
class Derived1 : public Base {
virtual void F();
};
class Derived2 : public Base {
virtual void F();
};
Una llamada hipotética b->F();
(con b
de tipo Base*
) es obviamente virtual. Pero usted (o el compiler ...) podría reescribirlo de esa manera (supongamos que typeof
es una función tipo typeid
que devuelve un valor que puede usarse en un switch
)
switch (typeof(b)) {
case Derived1: b->Derived1::F(); break; // static, inlineable call
case Derived2: b->Derived2::F(); break; // static, inlineable call
case Base: assert(!"pure virtual function call!");
default: b->F(); break; // virtual call (dyn-loaded code)
}
mientras que todavía necesitamos RTTI para el tipo de, la llamada puede ser inline efectivamente, básicamente, incrustando el vtable dentro de la secuencia de instrucciones y especializando la llamada para todas las clases involucradas. Esto también podría generalizarse al especializar solo unas pocas clases (digamos, solo Derived1
):
switch (typeof(b)) {
case Derived1: b->Derived1::F(); break; // hot path
default: b->F(); break; // default virtual call, cold path
}
C ++ 11 ha agregado final
. Esto cambia la respuesta aceptada: ya no es necesario conocer la clase exacta del objeto, es suficiente saber que el objeto tiene al menos el tipo de clase en el que se declaró la función final:
class A {
virtual void foo();
};
class B : public A {
inline virtual void foo() final { }
};
class C : public B
{
};
void bar(B const& b) {
A const& a = b; // Allowed, every B is an A.
a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C.
}
Con los compiladores modernos, no hará ningún daño para inliberarlos. Algunos combos antiguos de compilador / enlazador pueden haber creado varios vtables, pero no creo que ya sea un problema.
En los casos en que la llamada de función no es ambigua y la función es un candidato adecuado para la alineación, el compilador es lo suficientemente inteligente como para incorporar el código de todos modos.
El resto del tiempo "virtual en línea" es una tontería, y de hecho algunos compiladores no compilarán ese código.
Hay una categoría de funciones virtuales donde todavía tiene sentido tenerlas en línea. Considere el siguiente caso:
class Base {
public:
inline virtual ~Base () { }
};
class Derived1 : public Base {
inline virtual ~Derived1 () { } // Implicitly calls Base::~Base ();
};
class Derived2 : public Derived1 {
inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 ();
};
void foo (Base * base) {
delete base; // Virtual call
}
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 principal (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 la clase base o para cualquier conjunto de funciones donde la implementación derivada también llama a la implementación de las clases base.
He visto compiladores que no emiten ninguna tabla v si no existe ninguna función no en línea (y definida en un archivo de implementación en lugar de un encabezado). Lanzarían errores como la missing vtable-for-class-A
o algo similar, y estarías tan confundido como yo.
De hecho, eso no concuerda con el estándar, pero ocurre así que considere poner al menos una función virtual que no esté en el encabezado (si solo es el destructor virtual), para que el compilador pueda emitir un 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 mayoría de las veces lo usará cuando no conozca el tipo dinámico del objeto, porque esa fue la razón principal de lo virtual
en primer lugar.
Sin embargo, el compilador no puede ignorar por completo en inline
. Tiene otra semántica además de acelerar una función-llamada. La línea implícita para las definiciones en la clase es el mecanismo que le permite poner la definición en el encabezado: solo las funciones en inline
se pueden definir varias veces a lo largo de todo el programa sin infringir 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í.
Inlined declarado Las funciones virtuales están inline cuando son llamadas a través de objetos e ignoradas cuando son llamadas a través de un puntero o referencias.
Las funciones virtuales se pueden insertar a veces. Un extracto de la excelente faq de C ++ :
"La única vez que se puede insertar una llamada virtual en línea es cuando el compilador conoce la" clase exacta "del objeto que es el objetivo de la llamada a la función virtual. Esto solo puede ocurrir cuando el compilador tiene un objeto real en lugar de un puntero o referencia a un objeto, es decir, ya sea con un objeto local, un objeto global / estático o un objeto totalmente contenido dentro de un compuesto ".
Marcar un método virtual en línea, ayuda a optimizar aún más las funciones virtuales en dos casos siguientes:
Patrón de plantilla curiosamente recurrente ( http://www.codeproject.com/Tips/537606/Cplusplus-Prefer-Curiously-Recurring-Template-Patt )
Reemplazar los métodos virtuales con plantillas ( http://www.di.unipi.it/~nids/docs/templates_vs_inheritance.html )
Tiene sentido hacer funciones virtuales y luego llamarlas en objetos en lugar de referencias o punteros. Scott Meyer recomienda, en su libro "c ++ efectivo", nunca redefinir una función heredada no virtual. 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, puede usarlo incorrectamente en una fecha posterior. Por lo tanto, si realiza una función en una clase base y desea que sea redifinable, debe hacerlo virtual. Si tiene sentido hacer funciones virtuales y llamarlas a objetos, también tiene sentido insertarlas.
Un compilador solo puede alinear una función cuando la llamada se puede resolver sin ambigüedades en tiempo de 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).
en línea realmente no hace nada, es una pista. El compilador podría ignorarlo o podría alinear un evento de llamada sin estar en línea si ve la implementación y le gusta esta idea. Si la claridad del código está en juego, la línea debe eliminarse.