Fonction virtuelle pure avec implémentation

176

Ma compréhension de base est qu'il n'y a pas d'implémentation pour une fonction virtuelle pure, cependant, on m'a dit qu'il pourrait y avoir une implémentation pour une fonction virtuelle pure.

class A {
public:
    virtual void f() = 0;
};

void A::f() {
    cout<<"Test"<<endl;
}

Le code ci-dessus est-il OK?

Quel est le but d'en faire une fonction virtuelle pure avec une implémentation?

skydoor
la source

Réponses:

215

Une virtualfonction pure doit être implémentée dans un type dérivé qui sera directement instancié, mais le type de base peut toujours définir une implémentation. Une classe dérivée peut appeler explicitement l'implémentation de la classe de base (si les autorisations d'accès le permettent) en utilisant un nom complet (en appelant A::f()dans votre exemple - si l' A::f()était publicou protected). Quelque chose comme:

class B : public A {

    virtual void f() {
        // class B doesn't have anything special to do for f()
        //  so we'll call A's

        // note that A's declaration of f() would have to be public 
        //  or protected to avoid a compile time problem

        A::f();
    }

};

Le cas d'utilisation auquel je peux penser du haut de ma tête est celui où il existe un comportement par défaut plus ou moins raisonnable, mais le concepteur de classe souhaite que ce comportement par défaut ne soit invoqué qu'explicitement. Il peut également être le cas dans lequel vous souhaitez que les classes dérivées effectuent toujours leur propre travail mais soient également capables d'appeler un ensemble commun de fonctionnalités.

Notez que même si cela est permis par le langage, ce n'est pas quelque chose que je vois couramment utilisé (et le fait que cela puisse être fait semble surprendre la plupart des programmeurs C ++, même les plus expérimentés).

Michael Burr
la source
1
Vous avez oublié d'ajouter pourquoi cela surprend les programmeurs: c'est parce que la définition en ligne est interdite par la norme. Les définitions de méthodes virtuelles pures doivent être deported. (soit dans un .inl ou .cpp pour faire référence aux pratiques courantes de dénomination des fichiers).
v.oddou
donc cette méthode d'appel est identique à l'appel du membre de la méthode statique. Une sorte de méthode de classe en Java.
Sany Liew
2
"pas couramment utilisé" == mauvaise pratique? Je cherchais exactement le même comportement, en essayant d'implémenter NVI. Et NVI me semble une bonne pratique.
Saskia
5
Il vaut la peine de souligner que rendre A :: f () pur signifie que B doit implémenter f () (sinon B serait abstrait et non instable). Et comme le souligne @MichaelBurr, fournir une implémentation pour A :: f () signifie que B peut l' utiliser pour définir f ().
peurless_fool
2
IIRC, Scot Meyer a un excellent article sur le cas d'utilisation de cette question dans l'un de ses livres classiques "Plus efficace C ++"
irsis
75

Pour être clair, vous ne comprenez pas ce que = 0; après une fonction virtuelle signifie.

= 0 signifie que les classes dérivées doivent fournir une implémentation, pas que la classe de base ne puisse pas fournir une implémentation.

En pratique, lorsque vous marquez une fonction virtuelle comme pure (= 0), il est très peu utile de fournir une définition, car elle ne sera jamais appelée à moins que quelqu'un ne le fasse explicitement via Base :: Function (...) ou si le Le constructeur de classe de base appelle la fonction virtuelle en question.

Terry Mahaffey
la source
9
Ceci est une erreur. Si vous invoquez cette fonction virtuelle pure au niveau du constructeur de votre classe virtuelle pure, un appel virtuel pur serait effectué. Dans ce cas, il vaut mieux avoir une implémentation.
rmn
@rmn, Oui, vous avez raison sur les appels virtuels dans les constructeurs. J'ai mis à jour la réponse. Espérons que tout le monde sait ne pas faire cela, cependant. :)
Terry Mahaffey
3
En fait, faire un appel pur de base à partir d'un constructeur entraîne un comportement défini par l'implémentation. Dans VC ++, cela équivaut à un crash _purecall.
Ofek Shilon
@OfekShilon c'est correct - je serais tenté de l'appeler également comportement indéfini et candidat pour une mauvaise pratique / refactoring de code (c'est-à-dire appeler des méthodes virtuelles à l'intérieur du constructeur). Je suppose que cela a à voir avec la cohérence de la table virtuelle, qui pourrait ne pas être prête à être acheminée vers le corps de l'implémentation correcte.
teodron
1
Dans les constructeurs et les destructeurs, les fonctions virtuelles ne sont pas virtuelles.
Jesper Juhl
20

L'avantage est qu'il oblige les types dérivés à toujours remplacer la méthode, mais fournit également une implémentation par défaut ou additive.

JaredPar
la source
1
Pourquoi voudrais-je forcer s'il existe une implémentation par défaut? Cela ressemble à des fonctions virtuelles normales. S'il s'agissait simplement d'une fonction virtuelle normale, je peux soit remplacer et si je ne l'ai pas fait, une implémentation par défaut sera fournie (implémentation de base).
StackExchange123
19

Si vous avez du code qui devrait être exécuté par la classe dérivée, mais que vous ne voulez pas qu'il soit exécuté directement - et que vous voulez le forcer à être remplacé.

Votre code est correct, bien que dans l'ensemble ce ne soit pas une fonctionnalité souvent utilisée, et généralement visible uniquement lorsque vous essayez de définir un destructeur virtuel pur - dans ce cas, vous devez fournir une implémentation. Ce qui est drôle, c'est qu'une fois que vous dérivez de cette classe, vous n'avez pas besoin de remplacer le destructeur.

Par conséquent, la seule utilisation sensée des fonctions virtuelles pures est de spécifier un destructeur virtuel pur comme un mot clé "non final".

Le code suivant est étonnamment correct:

class Base {
public:
  virtual ~Base() = 0;
};

Base::~Base() {}

class Derived : public Base {};

int main() { 
  // Base b; -- compile error
  Derived d; 
}
Kornel Kisielewicz
la source
1
Les destructeurs de classe de base sont toujours appelés de toute façon, virtuels ou non et purs ou non; avec d'autres fonctions, vous ne pouvez pas garantir qu'une fonction virtuelle de substitution appellera l'implémentation de la classe de base, que la version de la classe de base soit pure ou non.
CB Bailey
1
Ce code est faux. Vous devez définir le dtor en dehors de la définition de classe en raison d'une bizarrerie de syntaxe du langage.
@Roger: merci, cela m'a vraiment aidé - c'est le code que j'utilise, il se compile bien sous MSVC, mais je suppose que ce ne serait pas portable.
Kornel Kisielewicz
4

Oui, c'est correct. Dans votre exemple, les classes qui dérivent de A héritent à la fois de l'interface f () et d'une implémentation par défaut. Mais vous forcez les classes dérivées à implémenter la méthode f () (même si ce n'est que pour appeler l'implémentation par défaut fournie par A).

Scott Meyers en parle dans Effective C ++ (2e édition) Item # 36 Différencier l'héritage de l'interface et l'héritage de l'implémentation. Le numéro d'article peut avoir changé dans la dernière édition.

Yukiko
la source
4

Les fonctions virtuelles pures avec ou sans corps signifient simplement que les types dérivés doivent fournir leur propre implémentation.

Les corps de fonction virtuelle purs dans la classe de base sont utiles si vos classes dérivées veulent appeler votre implémentation de classe de base.

Brian R. Bondy
la source
2

Le 'virtual void foo () = 0;' La syntaxe ne signifie pas que vous ne pouvez pas implémenter foo () dans la classe actuelle, vous pouvez. Cela ne signifie pas non plus que vous devez l'implémenter dans des classes dérivées . Avant de me gifler, observons le problème du diamant: (code implicite, remarquez).

class A
{
public: 
    virtual void foo()=0;
    virtual void bar();
}

class B : public virtual A
{
public:
    void foo() { bar(); }
}

class C : public virtual A
{
public:
    void bar();
}

class D : public B, public C
{}

int main(int argc, const char* argv[])
{
    A* obj = new D();
    **obj->foo();**
    return 0;
}

Maintenant, l'invocation obj-> foo () aboutira à B :: foo () puis à C :: bar ().

Vous voyez ... les méthodes virtuelles pures n'ont pas besoin d'être implémentées dans des classes dérivées (foo () n'a pas d'implémentation dans la classe C - le compilateur compilera) En C ++, il y a beaucoup de failles.

J'espère que je pourrais aider :-)

Nir Hedvat
la source
5
Il n'a pas besoin d'être implémenté dans TOUTES les classes dérivées, mais il DOIT avoir une implémentation dans toutes les classes dérivées que vous avez l'intention d'instancier. Vous ne pouvez pas instancier un objet de type Cdans votre exemple. Vous pouvez instancier un objet de type Dparce qu'il obtient sa mise en œuvre à foopartir B.
YoungJohn
0

Un cas d'utilisation important d' une méthode virtuelle pure avec un corps d'implémentation , est lorsque vous voulez avoir une classe abstraite, mais que vous n'avez aucune méthode appropriée dans la classe pour la rendre purement virtuelle. Dans ce cas, vous pouvez rendre le destructeur de la classe purement virtuel et mettre l'implémentation souhaitée (même un corps vide) pour cela. Par exemple:

class Foo
{
   virtual ~Foo() = 0;
   void bar1() {}
   void bar2(int x) {}
   // other methods
};

Foo::~Foo()
{
}

Cette technique rend la Fooclasse abstraite et par conséquent impossible d'instancier la classe directement. En même temps, vous n'avez pas ajouté de méthode virtuelle pure supplémentaire pour rendre la Fooclasse abstraite.

Gupta
la source