193 votes

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

Cette question m'a été posée lorsque j'ai reçu un commentaire de révision de code disant que les fonctions virtuelles ne doivent pas ê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 les objets. Mais le contre-argument qui m'est venu à l'esprit est le suivant : pourquoi voudrait-on définir des fonctions virtuelles et utiliser ensuite des objets pour appeler des méthodes ?

Est-il préférable de ne pas utiliser les fonctions virtuelles en ligne, puisqu'elles ne sont presque 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;
}

1 votes

Envisagez de compiler un exemple avec les commutateurs dont vous avez besoin pour obtenir un listing assembleur, puis montrez à l'examinateur du code que le compilateur peut effectivement intégrer des fonctions virtuelles.

1 votes

Ce qui précède ne sera généralement pas inlined, 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 était capable de signaler que pTemp->myVirtualFunction() pourrait être résolu comme un appel non virtuel, il pourrait avoir inline cet appel. Cet appel référencé est inline par g++ 3.4.2 : TempDerived & pTemp = aDerivedObj; pTemp.myVirtualFunction(); Votre code ne l'est pas.

1 votes

Une chose que gcc fait en fait est de comparer l'entrée de la table virtuelle à un symbole spécifique et ensuite d'utiliser une variante inlined dans une boucle si elle correspond. Ceci est particulièrement utile si la fonction inlined est vide et la boucle peut être éliminée dans ce cas.

167voto

ya23 Points 6491

Les fonctions virtuelles peuvent parfois être inlined. Un extrait de l'excellent FAQ sur le C++ :

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

10 votes

C'est vrai, mais il est bon de rappeler que le compilateur est libre d'ignorer le spécificateur inline même si l'appel peut être résolu au moment de la compilation et peut être inline.

6 votes

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 saute la résolution de la table virtuelle et la fonction devrait être inlined sans problèmes - pourquoi et si vous voulez le faire est un autre sujet :)

5 votes

@RnR. Il n'est pas nécessaire d'avoir 'this->', l'utilisation du nom qualifié est suffisante. 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).

85voto

MSalters Points 74024

C++11 a 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.
}

0 votes

Je n'ai pas pu le mettre en ligne dans VS 2017.

1 votes

Je ne pense pas que ça marche comme ça. L'invocation de foo() par un pointeur/référence de type A ne peut jamais être inlined. L'appel à b.foo() devrait permettre l'inlining. A moins que vous ne suggériez que le compilateur sache déjà qu'il s'agit d'un type B parce qu'il est conscient de la ligne précédente. Mais ce n'est pas l'usage typique.

0 votes

Par exemple, comparez le code généré pour la barre et le bas ici : godbolt.org/g/xy3rNh

39voto

Richard Corden Points 12292

Il existe une catégorie de fonctions virtuelles pour lesquelles il est toujours utile de les avoir en ligne. 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 à la suppression de 'base', effectuera un appel virtuel pour appeler le destructeur correct de la classe dérivée, cet appel n'est pas inlined. Cependant, étant donné que chaque destructeur appelle son destructeur parent (qui, dans ces cas, est vide), le compilateur peut inline ceux puisqu'ils n'appellent pas virtuellement les fonctions de la classe de base.

Le même principe existe pour les constructeurs des 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.

26 votes

Il faut cependant savoir que des accolades vides ne signifient pas toujours que le destructeur ne fait rien. Les destructeurs détruisent par défaut chaque objet membre de la classe, donc si vous avez quelques vecteurs dans la classe de base, cela peut représenter beaucoup de travail dans ces accolades vides !

15voto

J'ai vu des compilateurs qui n'émettent pas de 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 lançaient des erreurs comme missing vtable-for-class-A ou quelque chose de similaire, et vous seriez confus comme l'enfer, comme je l'étais.

En effet, ce n'est pas conforme à la norme, mais cela arrive donc pensez à mettre au moins une fonction virtuelle qui n'est 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 se produit avec certaines versions de gcc .

Comme quelqu'un l'a mentionné, les fonctions virtuelles en ligne peuvent être un avantage. parfois mais bien sûr, le plus souvent, vous l'utiliserez lorsque vous le ferez. pas connaître le type dynamique de l'objet, car c'était la raison d'être de virtual en premier lieu.

Le compilateur ne peut cependant pas ignorer complètement inline . Il a d'autres sémantiques que celle d'accélérer l'appel d'une fonction. Le site implicite en ligne pour les définitions en classe est le mécanisme qui vous permet de mettre la définition dans l'en-tête : Seulement inline Les fonctions peuvent être définies plusieurs fois dans l'ensemble du programme sans violation des règles. Au final, elle se comporte comme si vous ne l'aviez définie 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 entre eux.

13voto

CAFxX Points 3911

Eh bien, en fait les fonctions virtuelles peuvent toujours être inlined tant qu'elles sont statiquement liées entre elles : supposons que nous ayons une classe abstraite Base avec une fonction virtuelle F et les classes dérivées Derived1 y Derived2 :

class Base {
  virtual void F() = 0;
};

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

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

Un appel hypothétique b->F(); (avec b de type Base* ) est évidemment virtuelle. Mais vous (ou le compilateur ...) pourrait le réécrire comme suit (supposons que typeof est un typeid -qui renvoie une valeur pouvant être utilisée dans une fonction de type 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 inlined, en intégrant la vtable dans le flux d'instructions et en spécialisant l'appel pour toutes les classes concernées. Ceci pourrait aussi être généralisé en spécialisant seulement 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
}

1 votes

Existe-t-il des compilateurs qui font cela ? Ou est-ce une simple spéculation ? Désolé si je suis trop sceptique, mais votre ton dans la description ci-dessus ressemble à -- "ils pourraient tout à fait faire cela !", ce qui est différent de "certains compilateurs font cela".

1 votes

Oui, Graal fait de l'inlining polymorphique (également pour le bitcode LLVM via Sulong).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X