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

172

J'ai eu cette question lorsque j'ai reçu un commentaire de révision de code disant que les fonctions virtuelles ne doivent pas nécessairement être en ligne.

Je pensais que les fonctions virtuelles en ligne pourraient être utiles dans les scénarios où les fonctions sont appelées directement sur des objets. Mais le contre-argument m'est venu à l'esprit: pourquoi voudrait-on définir le virtuel puis utiliser des objets 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 de toute façon?

Extrait de code que j'ai utilisé pour l'analyse:

class Temp
{
public:

    virtual ~Temp()
    {
    }
    virtual void myVirtualFunction() const
    {
        cout<<"Temp::myVirtualFunction"<<endl;
    }

};

class TempDerived : public Temp
{
public:

    void myVirtualFunction() const
    {
        cout<<"TempDerived::myVirtualFunction"<<endl;
    }

};

int main(void) 
{
    TempDerived aDerivedObj;
    //Compiler thinks it's safe to expand the virtual functions
    aDerivedObj.myVirtualFunction();

    //type of object Temp points to is always known;
    //does compiler still expand virtual functions?
    //I doubt compiler would be this much intelligent!
    Temp* pTemp = &aDerivedObj;
    pTemp->myVirtualFunction();

    return 0;
}
un J.
la source
1
Envisagez de compiler un exemple avec les commutateurs dont vous avez besoin pour obtenir une liste d'assembleur, puis de montrer au réviseur de code que, en effet, le compilateur peut intégrer des fonctions virtuelles.
Thomas L Holaday
1
Ce qui précède ne sera généralement pas intégré, car vous appelez une fonction virtuelle à l'aide de la classe de base. Bien que cela ne dépende que de l'intelligence du compilateur. S'il pouvait indiquer que cela pTemp->myVirtualFunction()pouvait être résolu comme un appel non virtuel, il pourrait avoir en ligne cet appel. Cet appel référencé est incorporé par g ++ 3.4.2: TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction();Votre code ne l'est pas.
doc
1
Une chose que fait réellement gcc est de comparer l'entrée vtable à un symbole spécifique, puis d'utiliser une variante en ligne dans une boucle si elle correspond. Ceci est particulièrement utile si la fonction en ligne est vide et que la boucle peut être éliminée dans ce cas.
Simon Richter
1
@doc Le compilateur moderne s'efforce de déterminer au moment de la compilation les valeurs possibles des pointeurs. Le simple fait d'utiliser un pointeur n'est pas suffisant pour empêcher l'inlining à un niveau d'optimisation significatif; GCC effectue même des simplifications à l'optimisation zéro!
curiousguy

Réponses:

153

Les fonctions virtuelles peuvent parfois être intégrées. Un extrait de l'excellente FAQ C ++ :

"Le seul moment où un appel virtuel en ligne peut être inséré est lorsque le compilateur connaît la" classe exacte "de l'objet qui est la cible de l'appel de fonction virtuelle. Cela ne peut se produire que lorsque le compilateur a un objet réel plutôt qu'un pointeur ou référence à un objet. C'est-à-dire avec un objet local, un objet global / statique ou un objet entièrement contenu dans un composite. "

ya23
la source
7
C'est vrai, mais il convient de se rappeler que le compilateur est libre d'ignorer le spécificateur en ligne même si l'appel peut être résolu au moment de la compilation et peut être inséré.
Sharptooth
6
Une autre situation où je pense que l'inlining peut se produire est lorsque vous appelez la méthode par exemple comme this-> Temp :: myVirtualFunction () - une telle invocation ignore la résolution de la table virtuelle et la fonction devrait être intégrée sans problème - pourquoi et si vous ' Je veux le faire est un autre sujet :)
RnR
5
@RnR. Il n'est pas nécessaire d'avoir 'this->', il suffit d'utiliser le nom qualifié. Et ce comportement a lieu pour les destructeurs, les constructeurs et en général pour les opérateurs d'affectation (voir ma réponse).
Richard Corden le
2
Sharptooth - vrai, mais AFAIK cela est vrai de toutes les fonctions en ligne, pas seulement des fonctions virtuelles en ligne.
Colen
2
void f (const Base & lhs, const Base & rhs) {} ------ Dans l'implémentation de la fonction, vous ne savez jamais sur quoi pointent lhs et rhs avant l'exécution.
Baiyan Huang du
72

C ++ 11 a été ajouté final. Cela change la réponse acceptée: il n'est plus nécessaire de connaître la classe exacte de l'objet, il suffit de savoir que l'objet 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.
}
MSalters
la source
N'a pas pu l'intégrer dans VS 2017.
Yola
1
Je ne pense pas que cela fonctionne de cette façon. L'appel de foo () via un pointeur / référence de type A ne peut jamais être inséré. L'appel de b.foo () devrait autoriser l'inlining. Sauf si vous suggérez que le compilateur sait déjà qu'il s'agit d'un type B car il connaît la ligne précédente. Mais ce n'est pas l'usage typique.
Jeffrey Faust
Par exemple, comparez le code généré pour bar et bas ici: godbolt.org/g/xy3rNh
Jeffrey Faust
@JeffreyFaust Il n'y a aucune raison pour que l'information ne soit pas propagée, n'est-ce pas? Et iccsemble le faire, selon ce lien.
Alexey Romanov
Les compilateurs @AlexeyRomanov ont la liberté d'optimiser au-delà de la norme, et le font certainement! Pour les cas simples comme ci-dessus, le compilateur pourrait connaître le type et effectuer cette optimisation. Les choses sont rarement aussi simples et il n'est pas courant de pouvoir déterminer le type réel d'une variable polymorphe au moment de la compilation. Je pense qu'OP se soucie de «en général» et non de ces cas particuliers.
Jeffrey Faust
37

Il existe une catégorie de fonctions virtuelles où il est toujours logique de les avoir en ligne. Prenons 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 pour 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 (qui dans ces cas est vide), le compilateur peut incorporer 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.

Richard Corden
la source
23
Il faut cependant être conscient que les accolades vides ne signifient pas toujours que le destructeur ne fait rien. Les destructeurs détruisent par défaut tous les objets membres de la classe, donc si vous avez quelques vecteurs dans la classe de base, cela pourrait demander beaucoup de travail dans ces accolades vides!
Philip
14

J'ai vu des compilateurs qui n'émettent aucune v-table si aucune fonction non en ligne n'existe (et définie dans un fichier d'implémentation au lieu d'un en-tête alors). Ils jetteraient des erreurs comme missing vtable-for-class-Aou quelque chose de similaire, et vous seriez complètement confus, comme moi.

En effet, ce n'est pas conforme à la norme, mais cela arrive alors pensez à mettre au moins une fonction virtuelle pas dans l'en-tête (ne serait-ce que le destructeur virtuel), afin 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 dynamique de l'objet, car c'était la raison pour laquelle virtualen premier lieu.

Le compilateur ne peut cependant pas complètement ignorer inline. Il a une autre sémantique que l'accélération d'un appel de fonction. L' inline implicite pour les définitions en classe est le mécanisme qui vous permet de mettre la définition dans l'en-tête: seules les inlinefonctions peuvent être définies plusieurs fois dans l'ensemble du programme sans violation des règles. En fin de compte, il se comporte comme vous ne l'auriez défini qu'une seule fois dans l'ensemble du programme, même si vous avez inclus l'en-tête plusieurs fois dans différents fichiers liés entre eux.

Johannes Schaub - litb
la source
11

Eh bien, en fait, les fonctions virtuelles peuvent toujours être intégrées , tant qu'elles sont liées statiquement entre elles: supposons que nous ayons une classe abstraite Base avec une fonction virtuelle Fet des classes dérivées Derived1et 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 bde type Base*) est évidemment virtuel. Mais vous (ou le compilateur ...) pourriez le réécrire comme tel (supposons que ce typeofsoit une typeidfonction semblable à celle qui renvoie une valeur qui peut être utilisée dans a 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)
}

alors que nous avons toujours besoin de RTTI pour le typeof, l'appel peut effectivement être intégré en intégrant la vtable dans le flux d'instructions et en spécialisant l'appel pour toutes les classes impliquées. Cela pourrait également ê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
}
CAFxX
la source
Y a-t-il des compilateurs qui font cela? Ou est-ce juste une spéculation? Désolé si je suis trop sceptique, mais votre ton dans la description ci-dessus ressemble à - "ils pourraient tout à fait faire ça!", Ce qui est différent de "certains compilateurs font ça".
Alex Meiburg
Oui, Graal fait l'inlining polymorphe (également pour le bitcode LLVM via Sulong)
CAFxX
3

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

dents acérées
la source
2
Pour les compilateurs qui n'opèrent que sur des unités TU uniques, ils ne peuvent intégrer implicitement que les fonctions pour lesquelles ils ont la définition. Une fonction ne peut être définie dans plusieurs UT que si vous la définissez en ligne. 'inline' est plus qu'un indice et il peut avoir une amélioration spectaculaire des performances pour une construction g ++ / makefile.
Richard Corden
3

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

tarachandverma
la source
1

Avec les compilateurs modernes, cela ne fera aucun mal de les intégrer. Certains anciens combos compilateur / éditeur de liens ont peut-être créé plusieurs vtables, mais je ne pense plus que ce soit un problème.


la source
1

Un compilateur ne peut intégrer 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 au moment de l'exécution, et le compilateur ne peut donc pas intégrer l'appel, car au type de compilation, le type dynamique (et donc l'implémentation de la fonction à appeler) ne peut pas être déterminé.

PaulJWilliams
la source
1
Lorsque vous appelez une méthode de classe de base à partir de la même classe ou d'une classe dérivée, l'appel est sans ambiguïté et non virtuel
dents de scie
1
@sharptooth: mais alors ce serait une méthode en ligne non virtuelle. Le compilateur peut intégrer des fonctions auxquelles vous ne lui demandez pas, et il sait probablement mieux quand les intégrer ou non. Laissez-le décider.
David Rodríguez - dribeas le
1
@dribeas: Oui, c'est exactement ce dont je parle. Je me suis uniquement opposé à l'affirmation selon laquelle les finctions virtuelles sont résolues au moment de l'exécution - cela n'est vrai que lorsque l'appel est effectué virtuellement, pas pour la classe exacte.
Sharptooth
Je pense que c'est absurde. Toute fonction peut toujours être intégrée, quelle que soit sa taille ou qu'elle soit virtuelle ou non. Cela dépend de la manière dont le compilateur a été écrit. Si vous n'êtes pas d'accord, je m'attends à ce que votre compilateur ne puisse pas non plus produire de code non intégré. Autrement dit: le compilateur peut inclure du code qui lors de l'exécution teste les conditions qu'il n'a pas pu résoudre au moment de la compilation. C'est comme les compilateurs modernes peuvent résoudre des valeurs constantes / réduire les expressions nummeric au moment de la compilation. Si une fonction / méthode n'est pas en ligne, cela ne signifie pas qu'elle ne peut pas être intégrée.
1

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 incorporer le code de toute façon.

Le reste du temps "virtuel en ligne" est un non-sens, et en effet certains compilateurs ne compileront pas ce code.

l'ombre de la lune
la source
Quelle version de g ++ ne compilera pas les virtuels en ligne?
Thomas L Holaday
Hm. Le 4.1.1 que j'ai ici semble maintenant heureux. J'ai d'abord rencontré des problèmes avec cette base de code en utilisant un 4.0.x. Je suppose que mes informations sont obsolètes, modifiées.
moonshadow
0

Il est logique de créer des fonctions virtuelles, puis de les appeler sur des objets 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 que vous redéfinissez la fonction dans une classe dérivée, vous pouvez être sûr de l'utiliser correctement vous-même, mais vous ne pouvez pas être sûr que les autres l'utiliseront correctement. En outre, vous pouvez à une date ultérieure l'utiliser de manière incorrecte vous-même. Donc, si vous créez une fonction dans une classe de base et que vous voulez qu'elle soit redifinable, vous devez la rendre virtuelle. S'il est judicieux de créer des fonctions virtuelles et de les appeler sur des objets, il est également logique de les intégrer.

Balthazar
la source
0

En fait, dans certains cas, l'ajout «en ligne» à un remplacement final virtuel peut empêcher votre code de se compiler, il y a donc parfois une différence (au moins sous le compilateur VS2017)!

En fait, je faisais une fonction de remplacement final en ligne virtuelle dans VS2017 en ajoutant la norme c ++ 17 pour compiler et lier et pour une raison quelconque, cela a échoué lorsque j'utilise deux projets.

J'avais un projet de test et une DLL d'implémentation que je teste unitaire. Dans le projet de test, j'ai un fichier "linker_includes.cpp" qui #inclut les fichiers * .cpp de l'autre projet qui sont nécessaires. Je sais ... Je sais que je peux configurer msbuild pour utiliser les fichiers objets de la DLL, mais gardez à l'esprit qu'il s'agit d'une solution spécifique à Microsoft, tandis que l'inclusion des fichiers cpp n'est pas liée à build-system et beaucoup plus facile à version un fichier cpp que des fichiers xml et des paramètres de projet et autres ...

Ce qui était intéressant, c'est que j'obtenais constamment des erreurs de l'éditeur de liens du projet de test. Même si j'ai ajouté la définition des fonctions manquantes par copier-coller et non par include! Si étrange. L'autre projet a été construit et il n'y a aucun lien entre les deux autres que le marquage d'une référence de projet, il y a donc un ordre de construction pour s'assurer que les deux sont toujours construits ...

Je pense que c'est une sorte de bogue dans le compilateur. Je n'ai aucune idée s'il existe dans le compilateur livré avec VS2020, car j'utilise une version plus ancienne car certains SDK ne fonctionnent que correctement :-(

Je voulais juste ajouter que non seulement les marquer comme inline peut signifier quelque chose, mais peut même empêcher votre code de se construire dans de rares circonstances! C'est étrange, mais bon à savoir.

PS: Le code sur lequel je travaille est lié à l'infographie, donc je préfère l'inlining et c'est pourquoi j'ai utilisé à la fois final et inline. J'ai gardé le spécificateur final pour espérer que la version de la version est suffisamment intelligente pour créer la DLL en l'incrustant même sans que je le laisse directement entendre ...

PS (Linux).: Je pense que la même chose ne se produit pas dans gcc ou clang, car j'avais l'habitude de faire ce genre de choses. Je ne sais pas d'où vient ce problème ... Je préfère faire du C ++ sous Linux ou du moins avec certains gcc, mais parfois le projet a des besoins différents.

prenex
la source