c++ - reservadas - static int java
¿Cómo se beneficia el compilador de la nueva palabra clave final de C++? (3)
Dependiendo de cómo lo veas, hay un beneficio adicional para el compilador (aunque ese beneficio simplemente redunda en el usuario, por lo que podría decirse que este no es un beneficio del compilador): el compilador puede evitar emitir advertencias para construcciones con comportamiento incierto. ser anulable
Por ejemplo, considere este código:
class Base
{
public:
virtual void foo() { }
Base() { }
~Base();
};
void destroy(Base* b)
{
delete b;
}
Muchos compiladores emitirán una advertencia para el destructor no virtual de delete b
cuando se observe la delete b
. Si una clase Derived
heredara de la Base
y tuviera su propio ~Derived
, el uso de la destroy
en una instancia Derived
asignada dinámicamente normalmente (por comportamiento de especificación no está definido) llamará ~Base
, pero no llamará ~Derived
. Por lo tanto, las operaciones de limpieza de ~Derived
no ocurrirían, y eso podría ser malo (aunque probablemente no sea catastrófico, en la mayoría de los casos).
Sin embargo, si el compilador sabe que la Base
no se puede heredar, entonces no hay problema de que ~Base
no sea virtual, ya que no se puede omitir accidentalmente la limpieza derivada. La adición de final
a la class Base
le da al compilador la información para no emitir una advertencia.
Sé por un hecho que usar final
de esta manera suprimirá una advertencia con Clang. No sé si otros compiladores emiten una advertencia aquí, o si tienen en cuenta la finalidad de determinar si emitir o no una advertencia.
C ++ 11 permitirá que las clases y el método virtual sean definitivos para prohibir derivar de ellos o anularlos
class Driver {
virtual void print() const;
};
class KeyboardDriver : public Driver {
void print(int) const final;
};
class MouseDriver final : public Driver {
void print(int) const;
};
class Data final {
int values_;
};
Esto es muy útil, porque le dice al lector de la interfaz algo sobre la intención del uso de esta clase / método. Que el usuario obtenga diagnósticos si trata de anular también podría ser útil.
Pero ¿hay alguna ventaja desde el punto de vista de los compiladores? ¿Puede el compilador hacer algo diferente cuando sabe que "esta clase nunca se derivará de" o "esta función virtual nunca se anulará"?
Para final
encontré principalmente solo N2751 refiriéndose a él. Analizando algunas de las discusiones, encontré argumentos provenientes del lado de C ++ / CLI, pero no hay una pista clara de por qué final
puede ser útil para el compilador. Estoy pensando en esto, porque también veo algunas desventajas de marcar una final
clase: para realizar pruebas unitarias de las funciones de los miembros, se puede derivar una clase e insertar un código de prueba. A veces estas clases son buenas candidatas para ser marcadas con final
. Esta técnica sería imposible en estos casos.
Las llamadas virtuales a funciones son un poco más costosas que las llamadas normales. Además de realizar realmente la llamada, el tiempo de ejecución debe determinar primero a qué función llamar, qué frecuencia conduce a:
- Ubicar el puntero de la v-table y, a través de él, alcanzar la v-table
- Ubicar el puntero de función dentro de la tabla v, y a través de él realizar la llamada
En comparación con una llamada directa en la que la dirección de la función se conoce de antemano (y está codificada con un símbolo), esto lleva a una pequeña sobrecarga. Los buenos compiladores logran hacerlo solo un 10% -15% más lento que una llamada normal, lo que generalmente es insignificante si la función tiene alguna función.
El optimizador de un compilador todavía busca evitar todo tipo de gastos generales, y la llamada a la función de desvirtualización es generalmente una fruta de bajo rendimiento . Por ejemplo, ver en C ++ 03:
struct Base { virtual ~Base(); };
struct Derived: Base { virtual ~Derived(); };
void foo() {
Derived d; (void)d;
}
Clang obtiene:
define void @foo()() {
; Allocate and initialize `d`
%d = alloca i8**, align 8
%tmpcast = bitcast i8*** %d to %struct.Derived*
store i8** getelementptr inbounds ([4 x i8*]* @vtable for Derived, i64 0, i64 2), i8*** %d, align 8
; Call `d`''s destructor
call void @Derived::~Derived()(%struct.Derived* %tmpcast)
ret void
}
Como puede ver, el compilador ya era lo suficientemente inteligente como para determinar que d
es un Derived
, por lo que no es necesario incurrir en la sobrecarga de una llamada virtual.
De hecho, optimizaría la siguiente función igual de bien:
void bar() {
Base* b = new Derived();
delete b;
}
Sin embargo, hay algunas situaciones en las que el compilador no puede llegar a esta conclusión:
Derived* newDerived();
void deleteDerived(Derived* d) { delete d; }
Aquí podríamos esperar (ingenuamente) que una llamada a deleteDerived(newDerived());
resultaría en el mismo código que antes. Sin embargo, éste no es el caso:
define void @foobar()() {
%1 = tail call %struct.Derived* @newDerived()()
%2 = icmp eq %struct.Derived* %1, null
br i1 %2, label %_Z13deleteDerivedP7Derived.exit, label %3
; <label>:3 ; preds = %0
%4 = bitcast %struct.Derived* %1 to void (%struct.Derived*)***
%5 = load void (%struct.Derived*)*** %4, align 8
%6 = getelementptr inbounds void (%struct.Derived*)** %5, i64 1
%7 = load void (%struct.Derived*)** %6, align 8
tail call void %7(%struct.Derived* %1)
br label %_Z13deleteDerivedP7Derived.exit
_Z13deleteDerivedP7Derived.exit: ; preds = %3, %0
ret void
}
La convención podría dictar que newDerived
devuelve un Derived
, pero el compilador no puede hacer tal suposición: ¿y qué newDerived
si devolviera algo derivado? Y así podrá ver toda la maquinaria fea involucrada en la recuperación del puntero de la v-table, seleccionando la entrada apropiada en la tabla y finalmente realizando la llamada.
Sin embargo, si ponemos una final
, le damos al compilador una garantía de que no puede ser otra cosa:
define void @deleteDerived2(Derived2*)(%struct.Derived2* %d) {
%1 = icmp eq %struct.Derived2* %d, null
br i1 %1, label %4, label %2
; <label>:2 ; preds = %0
%3 = bitcast i8* %1 to %struct.Derived2*
tail call void @Derived2::~Derived2()(%struct.Derived2* %3)
br label %4
; <label>:4 ; preds = %2, %0
ret void
}
En resumen: final
permite que el compilador evite la sobrecarga de llamadas virtuales para las funciones en cuestión en situaciones en las que es imposible detectarlas.
Puedo pensar en un escenario en el que podría ser útil para el compilador desde una perspectiva de optimización. No estoy seguro de que valga la pena el esfuerzo de los implementadores del compilador, pero teóricamente es posible al menos.
Con virtual
despacho virtual
llamadas en un tipo final
derivado, puede estar seguro de que no hay nada más que se derive de ese tipo. Esto significa que (al menos en teoría) la palabra clave final
permitiría resolver correctamente algunas llamadas virtual
en tiempo de compilación, lo que haría posible una serie de optimizaciones que de otra manera serían imposibles en virtual
llamadas virtual
.
Por ejemplo, si ha delete most_derived_ptr
, donde most_derived_ptr
es un puntero a un tipo final
derivado, es posible que el compilador simplifique las llamadas al destructor virtual
.
Del mismo modo, para llamadas a funciones miembro virtual
en referencias / punteros al tipo más derivado.
Me sorprendería mucho si algún compilador hiciera esto hoy, pero parece ser el tipo de cosa que podría implementarse durante la próxima década.
También puede haber cierto impacto en poder inferir que (en ausencia de un friend
) las cosas marcadas como protected
en una class
final
también se vuelven private
.