sirve reservadas que programacion para palabras modificadores modificador constantes acceso c++ compiler-construction final c++11

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:

  1. Ubicar el puntero de la v-table y, a través de él, alcanzar la v-table
  2. 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 .