En C ++, ¿cuándo debo usar la declaración final en el método virtual?

11

Sé que la finalpalabra clave se usa para evitar que las clases derivadas anulen el método virtual. Sin embargo, no puedo encontrar ningún ejemplo útil cuando realmente debería usar finalpalabras clave con virtualmétodo. Aún más, parece que el uso de finalmétodos virtuales es un mal olor ya que no permite a los programadores extender la clase en el futuro.

Mi pregunta es la siguiente:

¿Hay algún caso útil cuando realmente debería usar finalen la virtualdeclaración del método?

metamaker
fuente
¿Por las mismas razones por las que los métodos Java se hacen finales?
Ordous
En Java todos los métodos son virtuales. El método final en la clase base puede mejorar el rendimiento. C ++ tiene métodos no virtuales, por lo que no es un caso para este lenguaje. ¿Conoces algún otro buen caso? Me gustaría escucharlos. :)
metamaker
Supongo que no soy muy bueno explicando cosas: estaba tratando de decir exactamente lo mismo que Mike Nakis.
Ordous

Respuestas:

12

Un resumen de lo que hace la finalpalabra clave: Digamos que tenemos una clase base Ay una clase derivada B. La función f()puede declararse Acomo virtual, lo que significa que la clase Bpuede anularla. Pero entonces clase Bpodría desear que cualquier clase que se deriva más lejos Bno pueda ser anulada f(). Ahí es cuando tenemos que declarar f()como finalen B. Sin la finalpalabra clave, una vez que definimos un método como virtual, cualquier clase derivada sería libre de anularlo. La finalpalabra clave se usa para poner fin a esta libertad.

Un ejemplo de por qué necesitaría tal cosa: suponga que la clase Adefine una función virtual prepareEnvelope(), y la clase la Banula y la implementa como una secuencia de llamadas a sus propios métodos virtuales stuffEnvelope(), lickEnvelope()y sealEnvelope(). La clase Btiene la intención de permitir que las clases derivadas anulen estos métodos virtuales para proporcionar sus propias implementaciones, pero la clase Bno quiere permitir que ninguna clase derivada anule prepareEnvelope()y, por lo tanto, cambie el orden de las cosas, lame, selle u omita la invocación de una de ellas. Entonces, en este caso, la clase se Bdeclara prepareEnvelope()como final.

Mike Nakis
fuente
Primero, gracias por la respuesta, pero ¿no es eso exactamente lo que he escrito en la primera oración de mi pregunta? Lo que realmente necesito es un caso cuando class B might wish that any class which is further derived from B should not be able to override f()? ¿Hay algún caso en el mundo real donde tal clase B y método f () exista y encaje bien?
metamaker
Sí, lo siento, espera, estoy escribiendo un ejemplo en este momento.
Mike Nakis
@metamaker ahí tienes.
Mike Nakis
1
Muy buen ejemplo, esta es la mejor respuesta hasta ahora. ¡Gracias!
metamaker
1
Entonces gracias por un tiempo perdido. No pude encontrar un buen ejemplo en Internet, perdí mucho tiempo buscando. Pondría +1 por la respuesta, pero no puedo hacerlo porque tengo poca reputación. :( Quizás más tarde.
metamaker
2

A menudo es útil desde una perspectiva de diseño poder marcar cosas como inmutables. Del mismo modo, constproporciona protectores del compilador e indica que un estado no debe cambiar, finalpuede usarse para indicar que el comportamiento no debería cambiar más abajo en la jerarquía de herencia.

Ejemplo

Considere un videojuego donde los vehículos llevan al jugador de un lugar a otro. Todos los vehículos deben verificar para asegurarse de que están viajando a un lugar válido antes de la salida (asegurándose de que la base en el lugar no esté destruida, por ejemplo). Podemos comenzar usando el lenguaje de interfaz no virtual (NVI) para garantizar que esta verificación se realice independientemente del vehículo.

class Vehicle
{
public:
    virtual ~Vehicle {}

    bool transport(const Location& location)
    {
        // Mandatory check performed for all vehicle types. We could potentially
        // throw or assert here instead of returning true/false depending on the
        // exceptional level of the behavior (whether it is a truly exceptional
        // control flow resulting from external input errors or whether it's
        // simply a bug for the assert approach).
        if (valid_location(location))
            return travel_to(location);

        // If the location is not valid, no vehicle type can go there.
        return false;
    }

private:
    // Overridden by vehicle types. Note that private access here
    // does not prevent derived, nonfriends from being able to override
    // this function.
    virtual bool travel_to(const Location& location) = 0;
};

Ahora digamos que tenemos vehículos voladores en nuestro juego, y algo que todos los vehículos voladores requieren y tienen en común es que deben pasar por una inspección de seguridad dentro del hangar antes del despegue.

Aquí podemos usar finalpara garantizar que todos los vehículos voladores pasarán por dicha inspección y también comunicar este requisito de diseño de los vehículos voladores.

class FlyingVehicle: public Vehicle
{
private:
    bool travel_to(const Location& location) final
    {
        // Mandatory check performed for all flying vehicle types.
        if (safety_inspection())
            return fly_to(location);

        // If the safety inspection fails for a flying vehicle, 
        // it will not be allowed to fly to the location.
        return false;
    }

    // Overridden by flying vehicle types.
    virtual void safety_inspection() const = 0;
    virtual void fly_to(const Location& location) = 0;
};

Al usarlo finalde esta manera, estamos efectivamente ampliando la flexibilidad del lenguaje de interfaz no virtual para proporcionar un comportamiento uniforme en la jerarquía de herencia (incluso como una ocurrencia tardía, contrarrestando el frágil problema de la clase base) a las funciones virtuales. Además, nos compramos espacio de maniobra para realizar cambios centrales que afectan a todos los tipos de vehículos voladores como una ocurrencia tardía sin modificar cada una de las implementaciones de vehículos voladores que existen.

Este es un ejemplo de uso final. Hay contextos que encontrará en los que simplemente no tiene sentido que una función miembro virtual se anule aún más; hacerlo podría conducir a un diseño frágil y una violación de sus requisitos de diseño.

Ahí es donde finales útil desde una perspectiva de diseño / arquitectura.

También es útil desde la perspectiva de un optimizador, ya que proporciona al optimizador esta información de diseño que le permite desvirtualizar las llamadas a funciones virtuales (eliminando la sobrecarga de despacho dinámico y, a menudo, de manera más significativa, eliminando una barrera de optimización entre la persona que llama y la persona que llama).

Pregunta

De los comentarios:

¿Por qué alguna vez se usaría final y virtual al mismo tiempo?

No tiene sentido que una clase base en la raíz de una jerarquía declare una función como ambos virtualy final. Eso me parece bastante tonto, ya que haría que tanto el compilador como el lector humano tengan que saltar a través de aros innecesarios que se pueden evitar simplemente evitando virtualdirectamente en tal caso. Sin embargo, las subclases heredan funciones de miembros virtuales de la siguiente manera:

struct Foo
{
   virtual ~Foo() {}
   virtual void f() = 0;
};

struct Bar: Foo
{
   /*implicitly virtual*/ void f() final {...}
};

En este caso, Bar::fusar explícitamente o no la palabra clave virtual, Bar::fes una función virtual. La virtualpalabra clave se convierte en opcional en este caso. Por lo que podría tener sentido para Bar::fser especificado como final, a pesar de que es una función virtual ( finalpuede solamente ser utilizado para funciones virtuales).

Y algunas personas pueden preferir, estilísticamente, indicar explícitamente que Bar::fes virtual, así:

struct Bar: Foo
{
   virtual void f() final {...}
};

Para mí es algo redundante usar ambos virtualy los finalespecificadores para la misma función en este contexto (del mismo modo virtualy override), pero es una cuestión de estilo en este caso. Algunas personas pueden encontrar que virtualaquí se comunica algo valioso, al igual que el uso externpara declaraciones de funciones con enlace externo (a pesar de que es opcional sin otros calificadores de enlace).


fuente
¿Cómo planeas anular el privatemétodo dentro de tu Vehicleclase? ¿No quisiste decir en su protectedlugar?
Andy
3
@DavidPacker privateno se extiende a la anulación (un poco contra-intuitivo). Los especificadores públicos / protegidos / privados para funciones virtuales solo se aplican a las personas que llaman y no a los anuladores, en pocas palabras. Una clase derivada puede anular las funciones virtuales de su clase base independientemente de su visibilidad.
@DavidPacker protectedpodría tener un sentido un poco más intuitivo. Simplemente prefiero alcanzar la menor visibilidad posible siempre que puedo. Creo que la razón por la que el lenguaje está diseñado de esta manera es que, de lo contrario, las funciones privadas virtuales de los miembros no tendrían ningún sentido fuera del contexto de la amistad, ya que ninguna clase sino un amigo podría anularlas si los especificadores de acceso importaran en el contexto de anular y no solo llamar.
Pensé que configurarlo en privado ni siquiera se compilaría. Por ejemplo, ni Java ni C # lo permiten. C ++ me sorprende todos los días. Incluso después de 10 años de programación.
Andy
@DavidPacker También me hizo tropezar cuando lo encontré por primera vez. Tal vez debería hacerlo protectedpara evitar confundir a los demás. Terminé poniendo un comentario, al menos, describiendo cómo las funciones virtuales privadas todavía se pueden anular.
0
  1. Permite muchas optimizaciones, porque puede conocerse en tiempo de compilación qué función se llama.

  2. Tenga cuidado al lanzar la palabra "olor a código". "final" no hace imposible extender la clase. Haga doble clic en la palabra "final", presione la tecla de retroceso y extienda la clase. SIN EMBARGO final es una excelente documentación de que el desarrollador no espera que anule la función, y que por eso el próximo desarrollador debe tener mucho cuidado, porque la clase podría dejar de funcionar correctamente si el método final se anula de esa manera.

gnasher729
fuente
¿Por qué se utilizaría alguna vez finaly virtualal mismo tiempo?
Robert Harvey
1
Permite muchas optimizaciones, porque puede conocerse en tiempo de compilación qué función se llama. ¿Puedes explicar cómo o proporcionar una referencia? SIN EMBARGO final es una excelente documentación de que el desarrollador no espera que anule la función, y que por eso el próximo desarrollador debe tener mucho cuidado, porque la clase podría dejar de funcionar correctamente si el método final se anula de esa manera. ¿No es un mal olor?
metamaker
@RobertHarvey ABI estabilidad. En cualquier otro caso, probablemente debería ser finaly override.
Deduplicador el
@metamaker Tuve la misma Q, discutida en los comentarios aquí, programmers.stackexchange.com/questions/256778/… , y curiosamente me he topado con varias otras discusiones, ¡solo desde que pregunté! Básicamente, se trata de si un compilador / enlazador de optimización puede determinar si una referencia / puntero podría representar una clase derivada y, por lo tanto, necesita una llamada virtual. Si el método (o clase) probablemente no se puede anular más allá del propio tipo estático de la referencia, pueden desvirtualizar: llamar directamente. Si hay alguna duda, tan a menudo, deben llamar virtualmente. Declarar finalrealmente puede ayudarlos
underscore_d