Mot clé "virtuel" C ++ pour les fonctions des classes dérivées. Est-ce nécessaire?

221

Avec la définition de structure 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() { ... }
};

Y a-t-il une différence entre ces deux façons de remplacer la fonction hello?

Anarki
la source
65
En C ++ 11, vous pouvez écrire "void hello () override {}" pour déclarer explicitement que vous remplacez une méthode virtuelle. Le compilateur échouera s'il n'existe pas de méthode virtuelle de base et a la même lisibilité que le placement de "virtual" sur la classe descendante.
ShadowChaser
En fait, dans C ++ 11 de gcc, l'écriture de void hello () override {} dans la classe dérivée est correcte car la classe de base a spécifié que la méthode hello () est virtuelle. En d'autres termes, l'utilisation du mot virtual dans la classe dérivée n'est pas nécessaire / obligatoire, pour gcc / g ++ de toute façon. (J'utilise gcc version 4.9.2 sur un RPi 3) Mais c'est quand même une bonne pratique d'inclure le mot clé virtual dans la méthode de la classe dérivée.
Will

Réponses:

183

Ce sont exactement les mêmes. Il n'y a pas de différence entre eux, sauf que la première approche nécessite plus de frappe et est potentiellement plus claire.

James McNellis
la source
25
C'est vrai, mais le Guide de portabilité de Mozilla C ++ recommande de toujours utiliser virtual car "certains compilateurs" émettent des avertissements si vous ne le faites pas. Dommage qu'ils ne mentionnent aucun exemple de tels compilateurs.
Sergei Tachenov
5
J'ajouterais également que le marquage explicite comme virtuel vous aidera à vous rappeler de rendre le destructeur virtuel également.
lfalin
1
Pour ne mentionner que, même applicable au destructeur virtuel
Atul
6
@SergeyTachenov selon le commentaire de clifford à sa propre réponse , un exemple de tels compilateurs est armcc.
Ruslan
4
@Rasmi, le nouveau guide de portabilité est là , mais maintenant il recommande d'utiliser le overridemot - clé.
Sergei Tachenov
83

La «virtualité» d'une fonction se propage implicitement, cependant au moins un compilateur que j'utilise générera un avertissement si le virtualmot-clé n'est pas utilisé explicitement, donc vous voudrez peut-être l'utiliser si ce n'est que pour garder le compilateur silencieux.

D'un point de vue purement stylistique, le virtualmot - clé «annonce» clairement le fait à l'utilisateur que la fonction est virtuelle. Cela sera important pour toute personne de la sous-classe B sans avoir à vérifier la définition de A. Pour les hiérarchies de classes profondes, cela devient particulièrement important.

Clifford
la source
12
De quel compilateur s'agit-il?
James McNellis
35
@James: armcc (compilateur ARM pour les appareils ARM)
Clifford
55

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

10.3 Fonctions virtuelles

2 Si une fonction membre virtuelle vfest déclarée dans une classe Baseet dans une classe Deriveddérivée directement ou indirectement d' Baseune fonction membre vfportant le même nom, la liste des types de paramètres (8.3.5), la qualification cv et le qualificatif ref ( ou son absence) tel qu'il Base::vfest déclaré, Derived::vfest alors également virtuel ( qu'il soit ou non déclaré ) et il l'emporte Base::vf.

R Sahu
la source
5
C'est de loin la meilleure réponse ici.
Fantastique Mr Fox
33

Non, le virtualmot clé sur les remplacements de fonctions virtuelles des classes dérivées n'est pas requis. Mais il convient de mentionner un écueil connexe: un échec à remplacer une fonction virtuelle.

L' échec de substitution se produit si vous avez l'intention de remplacer une fonction virtuelle dans une classe dérivée, mais faites une erreur dans la signature afin qu'elle déclare une fonction virtuelle nouvelle et différente. Cette fonction peut être une surcharge de la fonction de classe de base ou son nom peut différer. Que vous utilisiez ou non le virtualmot - clé dans la déclaration de fonction de classe dérivée, le compilateur ne pourra pas dire que vous avez l'intention de remplacer une fonction d'une classe de base.

Cet écueil est cependant heureusement résolu par la fonctionnalité de langage de substitution explicite C ++ 11 , 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 de compilation et l'erreur de programmation sera immédiatement évidente (peut-être que la fonction dans Derived aurait dû prendre un floatcomme argument).

Reportez-vous à WP: C ++ 11 .

Colin D Bennett
la source
11

L'ajout du mot clé "virtuel" est une bonne pratique car il 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.

Sujay Ghosh
la source
7

Il n'y a aucune différence pour le compilateur lorsque vous écrivez le virtualdans la classe dérivée ou l'omettez.

Mais vous devez regarder la classe de base pour obtenir ces informations. Par conséquent, je recommanderais d'ajouter le virtualmot - clé également dans la classe dérivée, si vous voulez montrer à l'humain que cette fonction est virtuelle.

harper
la source
2

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

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-interface plus tard pour définir des classes. Cela est utile pour l'interfonctionnement des interfaces entre les bibliothèques (ne comptez pas sur cela comme un processus de conception standard d'une seule bibliothèque). Cela ne vous coûte rien de permettre cela pour toutes vos classes - vous pouvez même typedefB à quelque chose si vous le souhaitez.

Notez que, si vous faites cela, vous voudrez peut-être aussi déclarer des constructeurs copier / déplacer en tant que modèles: autoriser la construction à partir d'interfaces différentes vous permet de «convertir» entre différents B<>types.

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

lorro
la source
1

Le virtualmot clé doit être ajouté aux fonctions d'une classe de base pour les rendre remplaçables. Dans votre exemple, struct Aest la classe de base. virtualne signifie rien pour utiliser ces fonctions dans une classe dérivée. Cependant, si vous voulez que votre classe dérivée soit également une classe de base elle-même et que vous voulez que cette fonction soit remplaçable, vous devrez alors la mettre virtuallà.

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

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

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

A
^
|
B
^
|
C

Vous devez donc mettre les virtualfonctions devant les classes de base potentielles qui peuvent avoir des enfants. virtualpermet à vos enfants de remplacer vos fonctions. Il n'y a rien de mal à mettre virtualen avant les 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 serait pas satisfait que la méthode de substitution ne fonctionne pas comme prévu.

Mettez donc virtualen avant les fonctions dans toutes les classes impliquées dans l'héritage, à moins que vous ne soyez certain que la classe n'aura pas d'enfants qui auraient besoin de remplacer les fonctions de la classe de base. C'est une bonne pratique.

Galaxie
la source
0

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

  • 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 supplémentaire appelle cette fonction virtuelle.
user2264698
la source
1
Je pense qu'il veut dire que sans marquer la fonction enfant comme virtuelle, un programmeur qui dérive de la classe enfant plus tard peut ne pas se rendre compte que la fonction est réellement virtuelle (car il n'a jamais regardé la classe de base) et peut potentiellement l'appeler pendant la construction ( qui peut ou non faire la bonne chose).
PfhorSlayer