web-dev-qa-db-fra.com

C ++ "virtual" mot-clé pour les fonctions dans les classes dérivées. Est-ce nécessaire?

Avec la définition de struct donnée ci-dessous ...

struct A {
    virtual void hello() = 0;
};

Approche n ° 1:

struct B : public A {
    virtual void hello() { ... }
};

Approche n ° 2:

struct B : public A {
    void hello() { ... }
};

Existe-t-il une différence entre ces deux manières de remplacer la fonction hello?

207
Anarki

Ils sont exactement les mêmes. Il n'y a pas de différence entre eux si ce n'est que la première approche nécessite plus de dactylographie et est potentiellement plus claire.

167
James McNellis

Le "caractère virtuel" d'une fonction est propagé de manière implicite. Cependant, au moins un compilateur que j'utilise génère un avertissement si le mot clé virtual n'est pas utilisé explicitement. Vous pouvez donc l'utiliser si vous voulez que le compilateur reste silencieux. .

D'un point de vue purement stylistique, le mot-clé virtual indique clairement que la fonction est virtuelle. Cela sera important pour quiconque sous-classe davantage B sans avoir à vérifier la définition de A. Pour les hiérarchies de classes profondes, cela devient particulièrement important.

81
Clifford

Le mot clé virtual n'est pas nécessaire dans la classe dérivée. Voici la documentation à l'appui, à partir du projet de norme C++ (N3337) (c'est moi qui souligne):

10.3 Fonctions virtuelles

2 Si une fonction membre virtuelle vf est déclarée dans une classe Base et dans une classe Derived, dérivée directement ou indirectement de Base, une fonction membre vf avec le même nom, la liste de types de paramètres (8.3.5), la qualification cv et le qualificateur de référence (ou l'absence de la même chose) que Base::vf est déclaré, puis Derived::vf est aussi virtuel (que cela soit ou non ainsi déclaré) et remplace Base::vf.

49
R Sahu

Non, le mot clé virtual relatif aux substitutions de fonction virtuelle des classes dérivées n'est pas requis. Mais il convient de mentionner un écueil connexe: l’échec de la substitution d’une fonction virtuelle.

L'échec à remplacer se produit si vous souhaitez remplacer une fonction virtuelle dans une classe dérivée, mais commettez une erreur dans la signature afin qu'elle en déclare une nouvelle et fonction virtuelle différente. Cette fonction peut être une surcharge de la fonction de classe de base ou son nom peut être différent. Que vous utilisiez ou non le mot clé virtual dans la déclaration de fonction de classe dérivée, le compilateur ne serait pas en mesure de dire que vous souhaitiez remplacer une fonction d'une classe de base.

Cet écueil est cependant heureusement résolu par la fonctionnalité de langage C++ 11 substitution explicite , qui permet au code source de spécifier clairement qu'une fonction membre est destinée à remplacer une fonction de classe de base:

struct Base {
    virtual void some_func(float);
};

struct Derived : Base {
    virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};

Le compilateur émettra une erreur lors de la compilation et l'erreur de programmation sera immédiatement évidente (la fonction dans Derived aurait peut-être dû prendre un argument comme argument float.).

Reportez-vous à WP: C++ 11 .

31
Colin D Bennett

L'ajout du mot clé "virtuel" est une bonne pratique car cela améliore la lisibilité, mais ce n'est pas nécessaire. Les fonctions déclarées virtuelles dans la classe de base et ayant la même signature dans les classes dérivées sont considérées comme "virtuelles" par défaut.

11
Sujay Ghosh

Il n'y a pas de différence pour le compilateur lorsque vous écrivez virtual dans la classe dérivée ou si vous l'omettez.

Mais vous devez consulter la classe de base pour obtenir ces informations. C'est pourquoi je recommanderais d'ajouter le mot clé virtual également dans la classe dérivée, si vous souhaitez montrer à l'homme que cette fonction est virtuelle.

7
harper

Il y a une différence considérable lorsque vous avez des modèles et commencez à prendre les classes de base comme paramètres de modèles:

struct None {};

template<typename... Interfaces>
struct B : public Interfaces
{
    void hello() { ... }
};

struct A {
    virtual void hello() = 0;
};

template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
    b.hello();   // indirect, non-virtual call
}

void hello(const A& a)
{
    a.hello();   // Indirect virtual call, inlining is impossible in general
}

int main()
{
    B<None>  b;         // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
    B<None>* pb = &b;
    B<None>& rb = b;

    b.hello();          // direct call
    pb->hello();        // pb-relative non-virtual call (1 redirection)
    rb->hello();        // non-virtual call (1 redirection unless optimized out)
    t_hello(b);         // works as expected, one redirection
    // hello(b);        // compile-time error


    B<A>     ba;        // Ok, vtable generated, sizeof(b) >= sizeof(void*)
    B<None>* pba = &ba;
    B<None>& rba = ba;

    ba.hello();         // still can be a direct call, exact type of ba is deducible
    pba->hello();       // pba-relative virtual call (usually 3 redirections)
    rba->hello();       // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
    //t_hello(b);       // compile-time error (unless you add support for const A& in t_hello as well)
    hello(ba);
}

La partie amusante est que vous pouvez maintenant définir des fonctions d’interface et non d’interface ultérieurement pour définir des classes. Cela est utile pour l’interfonctionnement d’interfaces entre bibliothèques (ne vous fiez pas à cela comme processus de conception standard d’une bibliothèque unique ). Cela ne vous coûte rien de permettre cela pour tous vos cours - vous pourriez même typedef B à quelque chose si vous le souhaitez.

Notez que si vous faites cela, vous voudrez peut-être aussi déclarer les constructeurs de copie/déplacement en tant que modèles: autoriser la construction à partir d'interfaces différentes vous permettra de "convertir" différents types B<>.

On peut se demander si vous devriez ajouter un support pour const A& Dans t_hello(). La raison habituelle de cette réécriture est de passer de la spécialisation basée sur l'héritage à celle basée sur des modèles, principalement pour des raisons de performances. Si vous continuez à prendre en charge l'ancienne interface, vous pouvez difficilement détecter (ou dissuader) un ancien usage.

1
lorro

Le mot-clé virtual doit être ajouté aux fonctions d'une classe de base pour pouvoir les remplacer. Dans votre exemple, struct A est la classe de base. virtual ne signifie rien pour utiliser ces fonctions dans une classe dérivée. Cependant, si vous voulez que votre classe dérivée soit aussi une classe de base elle-même et que cette fonction puisse être remplacée, vous devrez alors placer le virtual.

struct B : public A {
    virtual void hello() { ... }
};

struct C : public B {
    void hello() { ... }
};

Ici C hérite de B, donc B n'est pas la classe de base (c'est aussi une classe dérivée), et C est la classe dérivée. Le diagramme d'héritage ressemble à ceci:

A
^
|
B
^
|
C

Donc, vous devriez placer virtual devant des fonctions à l’intérieur de classes de base potentielles pouvant avoir des enfants. virtual permet à vos enfants de remplacer vos fonctions. Il n'y a rien de mal à placer le virtual devant des fonctions à l'intérieur des classes dérivées, mais ce n'est pas obligatoire. Il est cependant recommandé, car si quelqu'un souhaite hériter de votre classe dérivée, il ne sera pas ravi que le remplacement de la méthode ne fonctionne pas comme prévu.

Mettez donc virtual devant les fonctions de toutes les classes impliquées dans l'héritage, à moins que vous ne soyez sûr que la classe n'aura aucun enfant qui aurait besoin de remplacer les fonctions de la classe de base. C'est une bonne pratique.

1
Galaxy

Je vais certainement inclure le mot-clé Virtual pour la classe enfant, parce que

  • je. Lisibilité.
  • ii. Cette classe enfant peut être dérivée plus bas, vous ne voulez pas que le constructeur de la classe dérivée suivante appelle cette fonction virtuelle.
0
user2264698