Quel est le point d'une fonction virtuelle finale?
Wikipedia a l'exemple suivant sur le modificateur Final C++11:
struct Base2 {
virtual void f() final;
};
struct Derived2 : Base2 {
void f(); // ill-formed because the virtual function Base2::f has been marked final
};
Je ne comprends pas l'intérêt d'introduire une fonction virtuelle et de la marquer immédiatement comme finale. Est-ce simplement un mauvais exemple, ou est-il de plus?
10 réponses
Typiquement final
ne sera pas utilisé sur la définition d'une fonction virtuelle de la classe de base. final
sera utilisé par une classe dérivée qui remplace la fonction afin d'empêcher d'autres types dérivés de remplacer davantage la fonction. Puisque la fonction de substitution doit être virtuelle Normalement, cela signifierait que n'importe qui pourrait remplacer cette fonction dans un autre type dérivé. final
permet de spécifier une fonction qui remplace une autre mais qui ne peut pas être remplacée elle-même.
Pour exemple si vous concevez une hiérarchie de classes et que vous devez remplacer une fonction, mais que vous ne souhaitez pas autoriser les utilisateurs de la hiérarchie de classes à faire de même, vous pouvez marquer les fonctions comme finales dans vos classes dérivées.
Cela ne me semble pas utile du tout. Je pense que c'était juste un exemple pour démontrer la syntaxe.
Une utilisation possible est si vous ne voulez pas que f soit vraiment substituable, mais vous voulez toujours générer une vtable, mais c'est toujours une façon horrible de faire les choses.
Pour qu'une fonction soit étiquetée final
, elle doit être virtual
, c'est-à-dire en C++11 §10.3 para. 2:
[...] Pour plus de commodité, nous disons que toute fonction virtuelle se substitue à elle-même.
Et par. 4:
Si une fonction virtuelle f dans une classe B est marquée avec le spécificateur virt final et dans une classe D dérivée de B une fonction D::f remplace B::f, le programme est mal formé. [...]
C'est-à-dire, final
doit être utilisé avec des fonctions virtuelles (ou avec classes pour bloquer l'héritage) uniquement. Ainsi, l'exemple nécessite virtual
d'être utilisé pour qu'il soit un code C++ valide.
EDIT: pour être totalement clair: le "point"a demandé pourquoi le virtuel est même utilisé. La raison pour laquelle il est utilisé est (i) parce que le code ne compilerait pas autrement, et, (ii) pourquoi rendre l'exemple plus compliqué en utilisant plus de classes quand on suffit? Ainsi, exactement une classe avec une fonction finale virtuelle est utilisée comme exemple.
Je ne comprends pas l'intérêt d'introduire une fonction virtuelle et de la marquer immédiatement comme finale.
Le but de cet exemple est d'illustrer comment fonctionne final
, et il le fait exactement.
A pratique le but pourrait être de voir comment un vtable influence la taille d'une classe.
struct Base2 {
virtual void f() final;
};
struct Base1 {
};
assert(sizeof(Base2) != sizeof(Base1)); //probably
Base2
peut simplement être utilisé pour tester les spécificités de la plate-forme, et il ne sert à rien de remplacer f()
car il est là juste à des fins de test, donc il est marqué final
. De bien sûr, si vous faites cela, il y a quelque chose de mal dans la conception. Personnellement, je ne créerais pas une classe avec une fonction virtual
juste pour vérifier la taille du vfptr
.
Ajout aux bonnes réponses ci-dessus-voici une application bien connue de final (très inspirée de Java). Supposons que nous définissions une fonction wait () dans une classe de Base, et que nous ne voulions qu'une seule implémentation De wait () dans tous ses descendants. Dans ce cas, nous pouvons déclarer wait() comme définitive.
Par exemple:
class Base {
public:
virtual void wait() final { cout << "I m inside Base::wait()" << endl; }
void wait_non_final() { cout << "I m inside Base::wait_non_final()" << endl; }
};
Et voici la définition de la classe dérivée:
class Derived : public Base {
public:
// assume programmer had no idea there is a function Base::wait()
// error: wait is final
void wait() { cout << "I am inside Derived::wait() \n"; }
// that's ok
void wait_non_final() { cout << "I am inside Derived::wait_non_final(); }
}
Ce serait inutile (et non correct) si wait () était un virtuel pur la fonction. Dans ce cas: le compilateur vous demandera de définir wait() dans la classe dérivée. Si vous le faites, il vous donnera une erreur car wait() est définitive.
Pourquoi une fonction finale devrait-elle être virtuelle? (ce qui est également déroutant) parce que (imo) 1) Le concept de final est très proche du concept de fonctions virtuelles [les fonctions virtuelles ont de nombreuses implémentations - les fonctions finales n'ont qu'une seule implémentation], 2) Il est facile d'implémenter l'effet final en utilisant vtables.
Lors du refactoring du code hérité (par exemple en supprimant une méthode virtuelle d'une classe mère), ceci est utile pour s'assurer qu'aucune des classes enfants n'utilise cette fonction virtuelle.
// Removing foo method is not impacting any child class => this compiles
struct NoImpact { virtual void foo() final {} };
struct OK : NoImpact {};
// Removing foo method is impacting a child class => NOK class does not compile
struct ImpactChildClass { virtual void foo() final {} };
struct NOK : ImpactChildClass { void foo() {} };
int main() {}
Au lieu de ceci:
public:
virtual void f();
Je trouve utile d'écrire ceci:
public:
virtual void f() final
{
do_f(); // breakpoint here
}
protected:
virtual void do_f();
La raison principale étant que vous avez maintenant un seul endroit pour point d'arrêt avant de vous lancer dans l'une des implémentations potentiellement surchargées. Malheureusement (à mon humble avis), en disant "final"exige également que vous dites" virtuel."
J'ai trouvé un autre cas où la fonction virtuelle est utile pour être déclarée comme finale. Ce cas fait partie de SonarQube liste des avertissements . La description de l'avertissement dit:
L'appel d'une fonction membre substituable à partir d'un constructeur ou d'un destructeur peut entraîner un comportement inattendu lors de l'instanciation d'une sous-classe qui remplace la fonction membre.
Par exemple:
- Par contrat, le constructeur de classe de sous-classe commence par appeler la classe parente constructeur.
- Le constructeur de la classe parent appelle la fonction membre parent et non celle remplacée dans la classe enfant, ce qui est déroutant pour le développeur de la classe enfant.
- Il peut produire un comportement indéfini si la fonction membre est pure virtuelle dans la classe parent.
Exemple De Code Non Conforme
class Parent {
public:
Parent() {
method1();
method2(); // Noncompliant; confusing because Parent::method2() will always been called even if the method is overridden
}
virtual ~Parent() {
method3(); // Noncompliant; undefined behavior (ex: throws a "pure virtual method called" exception)
}
protected:
void method1() { /*...*/ }
virtual void method2() { /*...*/ }
virtual void method3() = 0; // pure virtual
};
class Child : public Parent {
public:
Child() { // leads to a call to Parent::method2(), not Child::method2()
}
virtual ~Child() {
method3(); // Noncompliant; Child::method3() will always be called even if a child class overrides method3
}
protected:
void method2() override { /*...*/ }
void method3() override { /*...*/ }
};
Solution Conforme
class Parent {
public:
Parent() {
method1();
Parent::method2(); // acceptable but poor design
}
virtual ~Parent() {
// call to pure virtual function removed
}
protected:
void method1() { /*...*/ }
virtual void method2() { /*...*/ }
virtual void method3() = 0;
};
class Child : public Parent {
public:
Child() {
}
virtual ~Child() {
method3(); // method3() is now final so this is okay
}
protected:
void method2() override { /*...*/ }
void method3() final { /*...*/ } // this virtual function is "final"
};
virtual
+ final
sont utilisés dans une déclaration de fonction pour rendre l'exemple plus court.
En ce qui concerne la syntaxe de virtual
et final
, L'exemple Wikipedia serait plus expressif en introduisant struct Base2 : Base1
avec Base1 contenant virtual void f();
et Base2 contenant void f() final;
(Voir ci-dessous).
Norme
Se Référant à N3690:
-
virtual
commefunction-specifier
peut être une partie dedecl-specifier-seq
-
final
peut faire partie devirt-specifier-seq
Il n'y a pas de règle avoir à utiliser le clé virtual
et la Identifiants avec une signification particulière final
ensemble. Sec 8.4, définitions de fonction (attention opt = optionnel):
Fonction-définition:
Attribut spécificateur-seq(opt) decl-spécificateur-seq(opt) déclaration de virt-spécificateur-seq(en option) la fonction-corps
Pratique
Avec C++11, Vous pouvez omettre le mot clé virtual
lorsque vous utilisez final
. Cette compile sur gcc >4.7.1, sur clang >3.0 avec C++11, sur msvc, ... (voir Explorateur du compilateur ).
struct A
{
virtual void f() {}
};
struct B : A
{
void f() final {}
};
int main()
{
auto b = B();
b.f();
}
PS: l'exemple sur cppreference n'utilise pas non plus virtual avec final dans la même déclaration.
PPS: il en va de même pour override
.
Ici, c'est pourquoi vous pouvez choisir de déclarer une fonction à la fois virtual
et final
dans une classe de base:
class A {
void f();
};
class B : public A {
void f(); // Compiles fine!
};
class C {
virtual void f() final;
};
class D : public C {
void f(); // Generates error.
};
Une fonction marquée final
a pour être aussi virtual
. Marquer une fonction final
vous empêche de déclarer une fonction avec le même nom et la même signature dans une classe dérivée.