¿Cuál es el propósito de la palabra clave "final" en C ++ 11 para funciones?

143

¿Cuál es el propósito de la finalpalabra clave en C ++ 11 para funciones? Entiendo que evita la anulación de funciones por clases derivadas, pero si este es el caso, ¿no es suficiente declarar como no virtuales sus finalfunciones? ¿Hay otra cosa que me falta aquí?

lezebulon
fuente
30
" ¿No es suficiente declarar como no virtuales sus" funciones finales " ? No, las funciones de anulación son implícitamente virtuales, ya sea que use la virtualpalabra clave o no.
ildjarn
13
@ildjarn eso no es cierto si no se declararon como virtuales en la superclase, no se puede derivar de una clase y transformar un método no virtual en uno virtual ..
Dan O
10
@ Dano, creo que no puedes anular, pero puedes "ocultar" un método de esa manera ... lo que genera muchos problemas, ya que la gente no quiere ocultar métodos.
Alex Kremer el
16
@DanO: Si no es virtual en la superclase, entonces no estaría "anulando".
ildjarn
2
Nuevamente, " anular " tiene un significado específico aquí, que es dar un comportamiento polimórfico a una función virtual. En su ejemplo funcno es virtual, por lo que no hay nada que anular y, por lo tanto, nada que marcar como overrideo final.
ildjarn

Respuestas:

129

Lo que se está perdiendo, como idljarn ya mencionado en un comentario es que si está anulando una función de una clase base, entonces no puede marcarla como no virtual:

struct base {
   virtual void f();
};
struct derived : base {
   void f() final;       // virtual as it overrides base::f
};
struct mostderived : derived {
   //void f();           // error: cannot override!
};
David Rodríguez - dribeas
fuente
¡Gracias! este es el punto que faltaba: es decir, que incluso sus clases "hoja" necesidad de marcar su función, ya que incluso virtual si tienen la intención de reemplazar las funciones, y no a ser anulado a sí mismos
lezebulon
8
@lezebulon: sus clases de hoja no necesitan marcar una función como virtual si la superclase la declaró virtual.
Dan O
55
Los métodos en las clases hoja son implícitamente virtuales si son virtuales en la clase base. Creo que los compiladores deberían advertir si falta este "virtual" implícito.
Aaron McDaid el
@AaronMcDaid: Los compiladores generalmente advierten sobre el código que, al ser correcto, puede causar confusión o errores. Nunca he visto a nadie sorprendido por esta característica particular del lenguaje de una manera que pueda causar algún problema, por lo que no sé cuán útil podría ser ese error. Por el contrario, olvidarse de que virtualpuede causar errores, y C ++ 11 agregó la overrideetiqueta a una función que detectará esa situación y no se compilará cuando una función que está destinada a anularse realmente se oculta
David Rodríguez - dribeas
1
De las notas de cambio de GCC 4.9: "Nuevo módulo de análisis de herencia de tipos que mejora la desvirtualización. La desvirtualización ahora tiene en cuenta los espacios de nombres anónimos y la palabra clave final C ++ 11", por lo que no es solo azúcar sintáctico, sino que también tiene un beneficio potencial de optimización.
kfsone
127
  • Es para evitar que una clase sea heredada. De Wikipedia :

    C ++ 11 también agrega la capacidad de evitar heredar de las clases o simplemente evitar la anulación de métodos en clases derivadas. Esto se hace con el identificador especial final. Por ejemplo:

    struct Base1 final { };
    
    struct Derived1 : Base1 { }; // ill-formed because the class Base1 
                                 // has been marked final
  • También se usa para marcar una función virtual para evitar que se anule en las clases derivadas:

    struct Base2 {
        virtual void f() final;
    };
    
    struct Derived2 : Base2 {
        void f(); // ill-formed because the virtual function Base2::f has 
                  // been marked final
    };

Wikipedia además hace un punto interesante :

Tenga en cuenta que overrideni finallas palabras clave de idioma son. Son técnicamente identificadores; solo adquieren un significado especial cuando se usan en esos contextos específicos . En cualquier otra ubicación, pueden ser identificadores válidos.

Eso significa que se permite lo siguiente:

int const final = 0;     // ok
int const override = 1;  // ok
Nawaz
fuente
1
gracias, pero olvidé mencionar que mi pregunta se refería al uso de "final" con los métodos
lezebulon
Lo mencionaste @lezebulon :-) "cuál es el propósito de la palabra clave" final "en C ++ 11 para funciones ". (mi énfasis)
Aaron McDaid el
¿Lo editaste? No veo ningún mensaje que diga "editado hace x minutos por lezebulon". ¿Cómo ocurrió eso? ¿Tal vez lo editó muy rápido después de enviarlo?
Aaron McDaid el
55
@ Aaron: las modificaciones realizadas dentro de los cinco minutos posteriores a la publicación no se reflejan en el historial de revisiones.
ildjarn
@Nawaz: ¿por qué no son palabras clave solo especificadores? ¿Es debido a razones de compatibilidad significa que es posible que el código preexistente antes de que C ++ 11 use final y anulación para otros fines?
Destructor
45

"final" también permite una optimización del compilador para evitar la llamada indirecta:

class IAbstract
{
public:
  virtual void DoSomething() = 0;
};

class CDerived : public IAbstract
{
  void DoSomething() final { m_x = 1 ; }

  void Blah( void ) { DoSomething(); }

};

con "final", el compilador puede llamar CDerived::DoSomething()directamente desde dentro Blah(), o incluso en línea. Sin ella, debe generar una llamada indirecta dentro de Blah()porque Blah()podría llamarse dentro de una clase derivada que se ha anulado DoSomething().

Chris Green
fuente
29

Nada que agregar a los aspectos semánticos de "final".

Pero me gustaría agregar al comentario de Chris Green que "final" podría convertirse en una técnica de optimización del compilador muy importante en un futuro no muy lejano. No solo en el caso simple que mencionó, sino también para jerarquías de clase real más complejas que pueden ser "cerradas" por "final", permitiendo así que los compiladores generen un código de despacho más eficiente que con el enfoque vtable habitual.

Una desventaja clave de vtables es que para cualquier objeto virtual de este tipo (suponiendo 64 bits en una CPU Intel típica), el puntero solo consume el 25% (8 de 64 bytes) de una línea de caché. En el tipo de aplicaciones que disfruto escribir, me duele mucho. (Y desde mi experiencia, es el argumento # 1 contra C ++ desde un punto de vista de rendimiento purista, es decir, por programadores de C).

En las aplicaciones que requieren un rendimiento extremo, lo que no es tan inusual para C ++, esto podría llegar a ser increíble, ya que no requiere solucionar este problema manualmente en estilo C o malabarismo de plantilla extraño.

Esta técnica se conoce como Desvirtualización . Un término que vale la pena recordar. :-)

Hay un gran discurso reciente de Andrei Alexandrescu que explica muy bien cómo puede solucionar tales situaciones hoy y cómo "final" podría ser parte de resolver casos similares "automáticamente" en el futuro (discutido con los oyentes):

http://channel9.msdn.com/Events/GoingNative/2013/Writing-Quick-Code-in-Cpp-Quickly

Mario Knezović
fuente
23
8 es 25% de 64?
ildjarn
66
¿Alguien sabe de un compilador que los utiliza ahora?
Vincent Fourmond
Lo mismo que quiero decir.
crazii
8

Final no se puede aplicar a funciones no virtuales.

error: only virtual member functions can be marked 'final'

No sería muy significativo poder marcar un método no virtual como 'final'. Dado

struct A { void foo(); };
struct B : public A { void foo(); };
A * a = new B;
a -> foo(); // this will call A :: foo anyway, regardless of whether there is a B::foo

a->foo()siempre llamare A::foo.

Pero, si A :: foo lo fuera virtual, entonces B :: foo lo anularía. Esto podría ser indeseable y, por lo tanto, tendría sentido hacer que la función virtual sea final.

La pregunta es, sin embargo, por qué permitir final en funciones virtuales. Si tienes una jerarquía profunda:

struct A            { virtual void foo(); };
struct B : public A { virtual void foo(); };
struct C : public B { virtual void foo() final; };
struct D : public C { /* cannot override foo */ };

Luego, finalpone un 'piso' sobre la cantidad de anulación que se puede hacer. Otras clases pueden extender A y B y anular sus foo, pero si una clase extiende C, entonces no está permitido.

Por lo tanto, probablemente no tenga sentido hacer el foo de "nivel superior" final, pero podría tener sentido más abajo.

(Sin embargo, creo que hay espacio para extender las palabras final y anular a los miembros no virtuales. Sin embargo, tendrían un significado diferente).

Aaron McDaid
fuente
gracias por el ejemplo, es algo de lo que no estaba seguro. Pero aún así: ¿cuál es el punto de tener una función final (y virtual)? Básicamente, usted nunca será capaz de utilizar el hecho de que la función es virtual ya que no puede ser sobreescrito
lezebulon
@lezebulon, edité mi pregunta. Pero entonces noté la respuesta de DanO: es una buena respuesta clara de lo que estaba tratando de decir.
Aaron McDaid
No soy un experto, pero creo que a veces puede tener sentido hacer una función de nivel superior final. Por ejemplo, si usted sabe que quiere todos los Shapes a foo()-algo predefinido y definida que ninguna forma derivada debe modificar. ¿O me equivoco y hay un patrón mejor para ese caso? EDITAR: ¿Oh, tal vez porque en ese caso, uno simplemente no debería llegar al nivel superior foo() virtualpara empezar? Pero aún así, puede ocultarse, incluso si se llama correctamente (polimórficamente) a través de Shape*...
Andrew Cheong
8

Un caso de uso para la palabra clave 'final' que me gusta es el siguiente:

// This pure abstract interface creates a way
// for unit test suites to stub-out Foo objects
class FooInterface
{
public:
   virtual void DoSomething() = 0;
private:
   virtual void DoSomethingImpl() = 0;
};

// Implement Non-Virtual Interface Pattern in FooBase using final
// (Alternatively implement the Template Pattern in FooBase using final)
class FooBase : public FooInterface
{
public:
    virtual void DoSomething() final { DoFirst(); DoSomethingImpl(); DoLast(); }
private:
    virtual void DoSomethingImpl() { /* left for derived classes to customize */ }
    void DoFirst(); // no derived customization allowed here
    void DoLast(); // no derived customization allowed here either
};

// Feel secure knowing that unit test suites can stub you out at the FooInterface level
// if necessary
// Feel doubly secure knowing that your children cannot violate your Template Pattern
// When DoSomething is called from a FooBase * you know without a doubt that
// DoFirst will execute before DoSomethingImpl, and DoLast will execute after.
class FooDerived : public FooBase
{
private:
    virtual void DoSomethingImpl() {/* customize DoSomething at this location */}
};
YoungJohn
fuente
1
Sí, este es esencialmente un ejemplo del patrón de método de plantilla. Y antes de C ++ 11, siempre fue el TMP lo que me hizo desear que C ++ tuviera una función de lenguaje como "final", como lo hizo Java.
Kaitain
6

final agrega una intención explícita de no anular su función, y provocará un error del compilador si esto se viola:

struct A {
    virtual int foo(); // #1
};
struct B : A {
    int foo();
};

Tal como está el código, se compila y se B::fooanula A::foo. B::fooTambién es virtual, por cierto. Sin embargo, si cambiamos el # 1 a virtual int foo() final, entonces este es un error del compilador, y no podemos anular A::foomás en las clases derivadas.

Tenga en cuenta que esto no nos permite "reabrir" una nueva jerarquía, es decir, no hay forma de crear B::foouna nueva función no relacionada que pueda estar independientemente al frente de una nueva jerarquía virtual. Una vez que una función es final, nunca se puede volver a declarar en ninguna clase derivada.

Kerrek SB
fuente
5

La palabra clave final le permite declarar un método virtual, anularlo N veces y luego ordenar que 'esto ya no se pueda anular'. Sería útil para restringir el uso de tu clase derivada, para que puedas decir "Sé que mi superclase te permite anular esto, pero si quieres derivar de mí, ¡no puedes!".

struct Foo
{
   virtual void DoStuff();
}

struct Bar : public Foo
{
   void DoStuff() final;
}

struct Babar : public Bar
{
   void DoStuff(); // error!
}

Como señalaron otros carteles, no se puede aplicar a funciones no virtuales.

Un propósito de la palabra clave final es evitar la anulación accidental de un método. En mi ejemplo, DoStuff () puede haber sido una función auxiliar que la clase derivada simplemente necesita cambiar de nombre para obtener el comportamiento correcto. Sin final, el error no se descubriría hasta la prueba.

Dan O
fuente
1

La palabra clave final en C ++ cuando se agrega a una función, evita que una clase base la anule. Además, cuando se agrega a una clase, se evita la herencia de cualquier tipo. Considere el siguiente ejemplo que muestra el uso del especificador final. Este programa falla en la compilación.

#include <iostream>
using namespace std;

class Base
{
  public:
  virtual void myfun() final
  {
    cout << "myfun() in Base";
  }
};
class Derived : public Base
{
  void myfun()
  {
    cout << "myfun() in Derived\n";
  }
};

int main()
{
  Derived d;
  Base &b = d;
  b.myfun();
  return 0;
}

También:

#include <iostream>
class Base final
{
};

class Derived : public Base
{
};

int main()
{
  Derived d;
  return 0;
}
Krishna Ganeriwal
fuente
0

Suplemento a la respuesta de Mario Knezović:

class IA
{
public:
  virtual int getNum() const = 0;
};

class BaseA : public IA
{
public:
 inline virtual int getNum() const final {return ...};
};

class ImplA : public BaseA {...};

IA* pa = ...;
...
ImplA* impla = static_cast<ImplA*>(pa);

//the following line should cause compiler to use the inlined function BaseA::getNum(), 
//instead of dynamic binding (via vtable or something).
//any class/subclass of BaseA will benefit from it

int n = impla->getNum();

El código anterior muestra la teoría, pero en realidad no se probó en compiladores reales. Aprecio mucho si alguien pega una salida desmontada.

crazii
fuente