En C ++, quand dois-je utiliser final dans la déclaration de méthode virtuelle?

11

Je sais que ce finalmot - clé est utilisé pour éviter que la méthode virtuelle ne soit remplacée par les classes dérivées. Cependant, je ne trouve aucun exemple utile quand je devrais vraiment utiliser un finalmot-clé avec une virtualméthode. De plus, il semble que l'utilisation de finalméthodes virtuelles soit une mauvaise odeur car elle interdit aux programmeurs d'étendre la classe à l'avenir.

Ma question est la suivante:

Existe-t-il un cas utile où je devrais vraiment utiliser finaldans la virtualdéclaration de méthode?

métamaker
la source
Pour les mêmes raisons que les méthodes Java deviennent définitives?
Ordous
En Java, toutes les méthodes sont virtuelles. La méthode finale dans la classe de base peut améliorer les performances. C ++ a des méthodes non virtuelles, donc ce n'est pas le cas pour ce langage. Connaissez-vous d'autres bons cas? J'aimerais les entendre. :)
métamaker
Je suppose que je ne suis pas très bon pour expliquer les choses - j'essayais de dire exactement la même chose que Mike Nakis.
Ordous

Réponses:

12

Un récapitulatif de ce que fait le finalmot-clé: Disons que nous avons une classe de base Aet une classe dérivée B. La fonction f()peut être déclarée en Atant que virtual, ce qui signifie que la classe Bpeut la remplacer. Mais alors, la classe Bpourrait souhaiter que toute classe qui en est dérivée Bne soit pas en mesure de remplacer f(). C'est quand nous avons besoin de déclarer f()comme finaldans B. Sans le finalmot - clé, une fois que nous avons défini une méthode comme virtuelle, toute classe dérivée serait libre de la remplacer. Le finalmot-clé est utilisé pour mettre fin à cette liberté.

Un exemple de la raison pour laquelle vous auriez besoin d'une telle chose: Supposons que la classe Adéfinit une fonction virtuelle prepareEnvelope(), et la classe la Bremplace et l'implémente comme une séquence d'appels à ses propres méthodes virtuelles stuffEnvelope(), lickEnvelope()et sealEnvelope(). La classe a l' Bintention de permettre aux classes dérivées de remplacer ces méthodes virtuelles pour fournir leurs propres implémentations, mais la classe Bne veut pas permettre à une classe dérivée de remplacer prepareEnvelope()et donc de changer l'ordre des éléments, de lécher, de sceller ou d'omettre d'invoquer l'une d'entre elles. Donc, dans ce cas, la classe Bdéclare prepareEnvelope()comme finale.

Mike Nakis
la source
Tout d'abord, merci pour la réponse, mais n'est-ce pas exactement ce que j'ai écrit dans une première phrase de ma question? Ce dont j'ai vraiment besoin, c'est d'un cas quand class B might wish that any class which is further derived from B should not be able to override f()? Existe-t-il un cas réel où une telle classe B et une telle méthode f () existent et s’adaptent bien?
metamaker
Oui, je suis désolé, attendez, j'écris un exemple en ce moment.
Mike Nakis
@metamaker vous y allez.
Mike Nakis
1
Très bon exemple, c'est la meilleure réponse à ce jour. THX!
metamaker
1
Alors merci pour le temps perdu. Je n'ai pas pu trouver un bon exemple sur Internet, j'ai perdu beaucoup de temps à chercher. Je mettrais +1 pour la réponse mais je ne peux pas le faire car j'ai une mauvaise réputation. :( Peut-être plus tard.
metamaker
2

Il est souvent utile du point de vue de la conception de pouvoir marquer les choses comme immuables. De la même manière, le constcompilateur fournit des protections et indique qu'un état ne doit pas changer, finalpeut être utilisé pour indiquer que le comportement ne doit pas changer plus bas dans la hiérarchie d'héritage.

Exemple

Imaginez un jeu vidéo où des véhicules conduisent le joueur d'un endroit à un autre. Tous les véhicules doivent vérifier pour s'assurer qu'ils se rendent à un endroit valide avant le départ (en s'assurant que la base à l'emplacement n'est pas détruite, par exemple). Nous pouvons commencer par utiliser l'idiome d'interface non virtuelle (NVI) pour garantir que cette vérification est effectuée quel que soit le véhicule.

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;
};

Maintenant, disons que nous avons des véhicules volants dans notre jeu, et quelque chose que tous les véhicules volants ont besoin et ont en commun, c'est qu'ils doivent passer un contrôle d'inspection de sécurité à l'intérieur du hangar avant le décollage.

Ici, nous pouvons utiliser finalpour garantir que tous les véhicules volants subiront une telle inspection et communiquer également cette exigence de conception des véhicules volants.

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;
};

En utilisant finalde cette manière, nous étendons effectivement la flexibilité de l'idiome d'interface non virtuelle pour fournir un comportement uniforme dans la hiérarchie d'héritage (même après coup, pour contrer le problème de la classe de base fragile) aux fonctions virtuelles elles-mêmes. De plus, nous nous achetons une marge de manœuvre pour effectuer des changements centraux qui affectent tous les types de véhicules volants après coup sans modifier chaque implémentation de véhicule volant qui existe.

Ceci est un exemple d'utilisation final. Il y a des contextes que vous rencontrerez où il est tout simplement insensé qu'une fonction de membre virtuel soit outrepassée davantage - cela pourrait entraîner une conception fragile et une violation de vos exigences de conception.

C'est là qu'il finalest utile d'un point de vue design / architectural.

Il est également utile du point de vue d'un optimiseur car il fournit à l'optimiseur ces informations de conception qui lui permettent de dévirtualiser les appels de fonction virtuels (en éliminant la surcharge de répartition dynamique, et souvent de manière plus significative, en éliminant une barrière d'optimisation entre l'appelant et l'appelé).

Question

D'après les commentaires:

Pourquoi le final et le virtuel seraient-ils jamais utilisés en même temps?

Il n'est pas logique pour une classe de base à la racine d'une hiérarchie de déclarer une fonction à la fois virtualet final. Cela me semble assez idiot, car cela obligerait à la fois le compilateur et le lecteur humain à sauter à travers des cerceaux inutiles qui peuvent être évités en évitant simplement virtualcarrément dans un tel cas. Cependant, les sous-classes héritent des fonctions de membre virtuel comme ceci:

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

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

Dans ce cas, l' Bar::futilisation explicite ou non du mot-clé virtuel Bar::fest une fonction virtuelle. Le virtualmot-clé devient alors facultatif dans ce cas. Il peut donc être judicieux de Bar::fspécifier as final, même s'il s'agit d'une fonction virtuelle ( finalne peut être utilisée que pour des fonctions virtuelles).

Et certaines personnes peuvent préférer, stylistiquement, indiquer explicitement que Bar::fc'est virtuel, comme ceci:

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

Pour moi, c'est un peu redondant d'utiliser les deux virtualet les finalspécificateurs pour la même fonction dans ce contexte (de même virtualet override), mais c'est une question de style dans ce cas. Certaines personnes pourraient trouver que virtualcommunique quelque chose de précieux ici, un peu comme utiliser externpour les déclarations de fonction avec liaison externe (même si c'est facultatif sans autres qualificatifs de liaison).


la source
Comment prévoyez-vous de remplacer la privateméthode dans votre Vehicleclasse? Tu ne voulais pas dire à la protectedplace?
Andy
3
@DavidPacker privatene s'étend pas à la surenchère (un peu contre-intuitive). Les spécificateurs publics / protégés / privés pour les fonctions virtuelles ne s'appliquent qu'aux appelants et non aux remplaçants, en gros. Une classe dérivée peut remplacer les fonctions virtuelles de sa classe de base quelle que soit sa visibilité.
@DavidPacker protectedpourrait avoir un sens un peu plus intuitif. Je préfère simplement atteindre la visibilité la plus faible possible chaque fois que je le peux. Je pense que la raison pour laquelle le langage est conçu de cette manière est que, sinon, les fonctions de membre virtuel privé n'auraient aucun sens en dehors du contexte de l'amitié, car aucune classe mais un ami ne pourrait alors les remplacer si les spécificateurs d'accès importaient dans le contexte de passer outre et pas seulement d'appeler.
Je pensais que le définir sur privé ne serait même pas compiler. Par exemple, ni Java ni C # ne le permettent. C ++ me surprend tous les jours. Même après 10 ans de programmation.
Andy
@DavidPacker Cela m'a aussi fait trébucher lorsque je l'ai rencontré pour la première fois. Peut-être que je devrais juste le faire protectedpour éviter de dérouter les autres. J'ai fini par mettre un commentaire, au moins, décrivant comment les fonctions virtuelles privées peuvent toujours être remplacées.
0
  1. Il permet de nombreuses optimisations, car il peut être connu au moment de la compilation quelle fonction est appelée.

  2. Soyez prudent en jetant le mot «odeur de code» autour. "final" ne rend pas impossible l'extension de la classe. Double-cliquez sur le mot «final», appuyez sur retour arrière et étendez la classe. CEPENDANT final est une excellente documentation que le développeur ne s'attend pas à ce que vous remplaciez la fonction, et qu'à cause de cela le prochain développeur devrait être très prudent, car la classe pourrait cesser de fonctionner correctement si la méthode finale est remplacée de cette façon.

gnasher729
la source
Pourquoi serait finalet virtualserait utilisé en même temps?
Robert Harvey
1
Il permet de nombreuses optimisations, car il peut être connu au moment de la compilation quelle fonction est appelée. Pouvez-vous expliquer comment ou fournir une référence? CEPENDANT final est une excellente documentation que le développeur ne s'attend pas à ce que vous remplaciez la fonction, et qu'à cause de cela le prochain développeur devrait être très prudent, car la classe pourrait cesser de fonctionner correctement si la méthode finale est remplacée de cette façon. N'est-ce pas une mauvaise odeur?
metamaker
@RobertHarvey stabilité ABI. Dans tous les autres cas, ce devrait probablement être finalet override.
Déduplicateur
@metamaker J'ai eu le même Q, discuté dans les commentaires ici - programmers.stackexchange.com/questions/256778/… - & drôlement ont trébuché dans plusieurs autres discussions, seulement depuis la demande! Fondamentalement, il s'agit de savoir si un compilateur / éditeur de liens d'optimisation peut déterminer si une référence / un pointeur peut représenter une classe dérivée et donc avoir besoin d'un appel virtuel. Si la méthode (ou la classe) ne peut pas être remplacée au-delà du propre type statique de la référence, ils peuvent dévirtualiser: appeler directement. S'il y a tout doute - aussi souvent - ils doivent appeler virtuellement. Déclarer finalpeut vraiment les aider
underscore_d