Les fonctions virtuelles en ligne sont-elles vraiment un non-sens?

J’ai eu cette question lorsque j’ai reçu un commentaire de révision de code indiquant que les fonctions virtuelles ne doivent pas nécessairement être intégrées.

Je pensais que les fonctions virtuelles en ligne pourraient être utiles dans les scénarios où les fonctions sont appelées directement sur les objects. Mais le contre-argument me vint à l’esprit: pourquoi voudrait-on définir le virtuel puis utiliser des objects pour appeler des méthodes?

Est-il préférable de ne pas utiliser les fonctions virtuelles en ligne, car elles ne sont pratiquement jamais développées?

Extrait de code utilisé pour l’parsing:

class Temp { public: virtual ~Temp() { } virtual void myVirtualFunction() const { cout<<"Temp::myVirtualFunction"<<endl; } }; class TempDerived : public Temp { public: void myVirtualFunction() const { cout<<"TempDerived::myVirtualFunction"<myVirtualFunction(); return 0; } 

Les fonctions virtuelles peuvent parfois être intégrées. Un extrait de l’excellent faq C ++ :

“Le seul moment où un appel virtuel en ligne peut être intégré est lorsque le compilateur connaît la” classe exacte “de l’object qui est la cible de l’appel de fonction virtuelle. Cela peut se produire uniquement lorsque le compilateur possède un object réel référence à un object, c’est-à-dire avec un object local, un object global / statique ou un object entièrement contenu dans un composite. ”

C ++ 11 a ajouté la final . Cela change la réponse acceptée: il n’est plus nécessaire de connaître la classe exacte de l’object, il suffit de savoir que l’object a au moins le type de classe dans lequel la fonction a été déclarée finale:

 class A { virtual void foo(); }; class B : public A { inline virtual void foo() final { } }; class C : public B { }; void bar(B const& b) { A const& a = b; // Allowed, every B is an A. a.foo(); // Call to B::foo() can be inlined, even if b is actually a class C. } 

Il existe une catégorie de fonctions virtuelles où il est toujours logique de les intégrer. Considérons le cas suivant:

 class Base { public: inline virtual ~Base () { } }; class Derived1 : public Base { inline virtual ~Derived1 () { } // Implicitly calls Base::~Base (); }; class Derived2 : public Derived1 { inline virtual ~Derived2 () { } // Implicitly calls Derived1::~Derived1 (); }; void foo (Base * base) { delete base; // Virtual call } 

L’appel à supprimer ‘base’, effectuera un appel virtuel pour appeler le destructeur de classe dérivé correct, cet appel n’est pas en ligne. Cependant, comme chaque destructeur appelle son destructeur parent (vide dans ce cas), le compilateur peut intégrer ces appels, car ils n’appellent pas virtuellement les fonctions de la classe de base.

Le même principe existe pour les constructeurs de classes de base ou pour tout ensemble de fonctions où l’implémentation dérivée appelle également l’implémentation des classes de base.

J’ai vu des compilateurs qui n’émettent aucune v-table si aucune fonction non-inline n’existe (et définie dans un fichier d’implémentation au lieu d’un en-tête). Ils jetteraient des erreurs comme le missing vtable-for-class-A ou quelque chose de similaire, et vous seriez confus comme l’enfer, comme je l’ missing vtable-for-class-A .

En effet, ce n’est pas conforme à la norme, mais cela arrive donc pensez à mettre au moins une fonction virtuelle dans l’en-tête (si seulement le destructeur virtuel), de sorte que le compilateur puisse émettre une vtable pour la classe à cet endroit. Je sais que cela arrive avec certaines versions de gcc .

Comme quelqu’un l’a mentionné, les fonctions virtuelles en ligne peuvent parfois être un avantage, mais bien sûr, le plus souvent, vous les utiliserez lorsque vous ne connaissez pas le type dynamic de l’object, car c’est la raison d’être de virtual .

Le compilateur ne peut cependant pas complètement ignorer en inline . Il a d’autres sémantiques en plus d’accélérer un appel de fonction. Le paramètre inline implicite pour les définitions en classe est le mécanisme qui vous permet de placer la définition dans l’en-tête: Seules inline fonctions en inline peuvent être définies plusieurs fois dans l’ensemble du programme sans violation de règles. En fin de compte, il se comporte comme si vous ne l’aviez défini qu’une seule fois dans tout le programme, même si vous avez inclus l’en-tête plusieurs fois dans différents fichiers liés ensemble.

Eh bien, en fait, les fonctions virtuelles peuvent toujours être alignées , tant qu’elles sont liées entre elles de manière statique: supposons que nous ayons une classe abstraite Base avec une fonction virtuelle F et des classes dérivées Derived1 et Derived2 :

 class Base { virtual void F() = 0; }; class Derived1 : public Base { virtual void F(); }; class Derived2 : public Base { virtual void F(); }; 

Un appel hypotétique b->F(); (avec b de type Base* ) est évidemment virtuel. Mais vous (ou le compilateur …) pourriez le réécrire comme cela (supposons que typeof est une fonction de type typeid qui retourne une valeur pouvant être utilisée dans un switch )

 switch (typeof(b)) { case Derived1: b->Derived1::F(); break; // static, inlineable call case Derived2: b->Derived2::F(); break; // static, inlineable call case Base: assert(!"pure virtual function call!"); default: b->F(); break; // virtual call (dyn-loaded code) } 

typeof que nous ayons toujours besoin de RTTI pour le typeof , l’appel peut effectivement être intégré en incorporant essentiellement la vtable dans le stream d’instructions et en spécialisant l’appel pour toutes les classes impliquées. Cela pourrait aussi être généralisé en ne spécialisant que quelques classes (disons juste Derived1 ):

 switch (typeof(b)) { case Derived1: b->Derived1::F(); break; // hot path default: b->F(); break; // default virtual call, cold path } 

inline ne fait vraiment rien – c’est un indice. Le compilateur peut l’ignorer ou il peut incorporer un événement d’appel sans inline s’il voit l’implémentation et aime cette idée. Si la clarté du code est en jeu, le fichier en ligne doit être supprimé.

Les fonctions virtuelles déclarées sont incorporées lorsqu’elles sont appelées via des objects et ignorées lorsqu’elles sont appelées via un pointeur ou des références.

Marquer une méthode virtuelle en ligne permet d’optimiser davantage les fonctions virtuelles dans les deux cas suivants:

Avec les compilateurs modernes, cela ne nuira pas à les inlibérer. Certains anciens combos compilateur / éditeur de liens peuvent avoir créé plusieurs vtables, mais je ne pense pas que ce soit un problème.

Dans les cas où l’appel de fonction est sans ambiguïté et la fonction un candidat approprié pour l’inlining, le compilateur est assez intelligent pour intégrer le code de toute façon.

Le rest du temps, “virtual inline” est un non-sens, et en effet certains compilateurs ne comstackront pas ce code.

Un compilateur ne peut incorporer une fonction que lorsque l’appel peut être résolu sans ambiguïté au moment de la compilation.

Les fonctions virtuelles, cependant, sont résolues à l’exécution, et le compilateur ne peut donc pas incorporer l’appel, car au type de compilation, le type dynamic (et donc l’implémentation de la fonction à appeler) ne peut pas être déterminé.

Il est logique de créer des fonctions virtuelles, puis de les appeler sur des objects plutôt que sur des références ou des pointeurs. Scott Meyer recommande, dans son livre “effective c ++”, de ne jamais redéfinir une fonction non virtuelle héritée. Cela a du sens, car lorsque vous créez une classe avec une fonction non virtuelle et redéfinissez la fonction dans une classe dérivée, vous pouvez être sûr de l’utiliser vous-même correctement, mais vous ne pouvez pas être sûr que d’autres l’utiliseront correctement. En outre, vous pouvez l’utiliser ultérieurement de manière incorrecte. Donc, si vous faites une fonction dans une classe de base et que vous voulez qu’elle soit redifinable, vous devriez la rendre virtuelle. S’il est logique de créer des fonctions virtuelles et de les appeler sur des objects, il est également judicieux de les intégrer.