web-dev-qa-db-fra.com

Interface C++ vs modèle

J'ai 2 solutions pour le même problème - faire une sorte de rappel d'un "contrôleur" à l'objet utilisé et je ne sais pas quoi choisir.

Solution 1: utiliser des interfaces

struct AInterface
{
    virtual void f() = 0;
};

struct A : public AInterface
{
    void f(){std::cout<<"A::f()"<<std::endl;}
};

struct UseAInterface
{
    UseAInterface(AInterface* a) : _a(a){}
    void f(){_a->f();}

    AInterface* _a;
};

Solution 2: utiliser des modèles

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

template<class T>
struct UseA
{
    UseA(T* a) : _a(a){}
    void f(){_a->f();}

    T* _a;
};

Ceci est juste un exemple simple pour illustrer mon problème. Dans le monde réel, l'interface aura plusieurs fonctions et une classe peut (et va!) Implémenter plusieurs interfaces.

Le code ne sera pas utilisé en tant que bibliothèque pour des projets externes et je n'ai pas à masquer l'implémentation du modèle. Je le dis car le premier cas sera préférable si je dois masquer l'implémentation du "contrôleur".

Pouvez-vous s'il vous plaît me dire les avantages/inconvénients pour chaque cas et ce qui est préférable d'utiliser?

27
Felics

À mon avis, les performances devraient être ignorées (pas vraiment, mais les micro-optimisations le devraient) jusqu'à ce que vous en ayez la raison. Sans quelques exigences strictes (il s’agit d’une boucle serrée occupant la majeure partie du processeur, les implémentations réelles des fonctions des membres de l’interface sont très petites ...), il serait très difficile, voire impossible, de remarquer la différence.

Je me concentrerais donc sur un niveau de conception supérieur. Est-il logique que tous les types utilisés dans UseA partagent une base commune? Sont-ils vraiment liés? Y a-t-il une relation claire est-a / entre les types? Ensuite, l’approche OO pourrait fonctionner. Sont-ils indépendants? Autrement dit, partagent-ils certains traits, mais il n'y a pas de relation directe est-un que vous puissiez modéliser? Optez pour l'approche modèle.

Le principal avantage du modèle est que vous pouvez utiliser des types qui ne sont pas conformes à une hiérarchie d'héritage particulière et exacte. Par exemple, vous pouvez stocker tout ce qui se trouve dans un vecteur constructible par copie (constructible par déplacement en C++ 11), mais int et Car ne sont aucunement liés entre eux. De cette façon, vous réduisez le couplage entre les différents types utilisés avec votre type UseA.

L'un des inconvénients des modèles est que chaque instanciation de modèle est d'un type différent, sans rapport avec le reste des instanciations de modèle générées à partir du même modèle de base. Cela signifie que vous ne pouvez pas stocker UseA<A> et UseA<B> dans le même conteneur, il y aura code-bloat (UseA<int>::foo et UseA<double>::foo sont générés dans le binaire), des temps de compilation plus longs (même sans prendre en compte les fonctions supplémentaires, deux unités de traduction) cette utilisation UseA<int>::foo générera la même fonction et l’éditeur de liens devra en éliminer une).

En ce qui concerne la performance revendiquée par les autres réponses, elles ont certes raison, mais la plupart passent à côté des points importants. Le principal avantage du choix des modèles par rapport à la répartition dynamique ne réside pas dans la surcharge supplémentaire de la répartition dynamique, mais dans le fait que de petites fonctions peuvent être insérées par le compilateur (si la définition de la fonction elle-même est visible). 

Si les fonctions ne sont pas en ligne, à moins que la fonction ne prenne que très peu de cycles à exécuter, le coût total de la fonction l'emportera sur le coût supplémentaire d'envoi dynamique (c'est-à-dire l'indirection supplémentaire dans l'appel et le décalage possible du pointeur this dans la cas d'héritage multiple/virtuel). Si les fonctions effectuent un travail réel et/ou si elles ne peuvent pas être en ligne, elles auront les mêmes performances.

Même dans les rares cas où la différence de performance d’une approche à l’autre pourrait être mesurable (supposons que les fonctions ne prennent que deux cycles et que l’envoi double donc le coût de chaque fonction) si ce code fait partie des 80% de le code qui prend moins de 20% du temps CPU, et dit que ce morceau de code en particulier prend 1% du CPU (ce qui est une somme énorme si l'on considère le principe que pour que les performances soient remarquables, la fonction elle-même doit prendre juste un ou deux cycles!), vous parlez alors de 30 secondes sur une heure de programme. En vérifiant à nouveau les lieux, sur un processeur de 2 GHz, 1% du temps signifie que la fonction devrait être appelée plus de 10 millions de fois par seconde.

Tout ce qui précède est agité à la main, et il va dans le sens inverse des autres réponses (c’est-à-dire que certaines imprécisions pourraient donner l’impression que la différence est plus petite qu’elle ne l’est réellement, mais la réalité est plus proche de celle-ci que ce n’est le cas. à la réponse générale l'envoi dynamique rendra votre code plus lent .

Il y a des avantages et des inconvénients pour chacun. Depuis le Langage de programmation C++ :

  1. Préférez un modèle aux classes dérivées lorsque l'efficacité d'exécution est primordiale.
  2. Préférez les classes dérivées à un modèle si l'ajout de nouvelles variantes sans recompilation est important.
  3. Préférez un modèle aux classes dérivées lorsqu'aucune base commune ne peut être définie.
  4. Préférez un modèle aux classes dérivées lorsque les types intégrés et les structures avec des contraintes de compatibilité sont importants.

Cependant, les modèles ont leurs inconvénients

  1. Le code utilisant les interfaces OO peut être masqué dans les fichiers .cpp/.CC, chaque fois que les modèles obligent à exposer tout le code dans le fichier d'en-tête.
  2. Les modèles vont provoquer un gonflement du code;
  3. Les interfaces OO sont explicites, chaque fois que les exigences relatives aux paramètres de modèle sont implicites et n'existent que dans la tête du développeur;
  4. Une utilisation intensive des modèles nuit à la vitesse de compilation.

Lequel à utiliser dépend de votre situation et de vos préférences. Le code basé sur un modèle peut produire des erreurs de compilation obtuses qui ont conduit à des outils tels que STL Error decrypt . Espérons que les concepts seront bientôt mis en œuvre.

19
Steve

Les performances du modèle seront légèrement meilleures, car aucun appel virtuel n’est impliqué. Si le rappel est utilisé très fréquemment, privilégiez la solution de modèle. Notez que "très fréquemment" ne commence pas vraiment avant que des milliers de secondes ne soient impliqués, probablement même plus tard.

D'autre part, le modèle doit être dans un fichier d'en-tête, ce qui signifie que chaque modification apportée obligera à recompiler tous les sites qui l'appellent, contrairement au scénario d'interface, où l'implémentation pourrait être dans un fichier .cpp et être le seul fichier nécessitant recompilation.

18
Angew

Vous pourriez envisager une interface comme un contrat. Toute classe qui en dérive doit implémenter les méthodes de l'interface.

Les modèles, d’autre part, implicitement ont certaines contraintes. Par exemple, votre paramètre de modèle T doit avoir une méthode f. Ces exigences implicites doivent être documentées avec soin, les messages d'erreur impliquant des modèles peuvent être assez déroutants.

Boost Concept peut être utilisé pour la vérification de concept, ce qui facilite la compréhension des exigences du modèle implcit.

4
Dirk

Le choix que vous décrivez est le choix entre le polymorphisme statique et le polymorphisme dynamique. Vous trouverez de nombreuses discussions sur ce sujet si vous recherchez cela.

Il est difficile de donner une réponse spécifique à une question aussi générale. En général, le polymorphisme statique peut vous offrir de meilleures performances, mais l'absence de concepts dans la norme C++ 11 signifie également que vous pouvez obtenir des messages d'erreur intéressant compilateur lorsqu'une classe ne modélise pas le concept requis.

1
dhavenith

Je voudrais aller avec la version du modèle. Si vous pensez à cela en termes de performances, alors cela a du sens.

Interface virtuelle - Utiliser virtuel signifie que la mémoire de la méthode est dynamique et qu’elle est définie au moment de l’exécution. Cela entraîne une surcharge en ce sens qu'il doit consulter la table vlookup pour localiser cette méthode en mémoire.

Modèles - Vous obtenez un mappage statique. Cela signifie que lorsque votre méthode est appelée, elle n'a pas à consulter la table de consultation et est déjà au courant de l'emplacement de la méthode en mémoire.

Si les performances vous intéressent, les modèles sont presque toujours le choix idéal.

0
Freddy