Idiome Pimpl vs interface de classe virtuelle pure

118

Je me demandais ce qui inciterait un programmeur à choisir l'idiome Pimpl ou la classe virtuelle pure et l'héritage.

Je comprends que l'idiome pimpl est livré avec une indirection supplémentaire explicite pour chaque méthode publique et la surcharge de création d'objet.

La classe virtuelle Pure en revanche est livrée avec une indirection implicite (vtable) pour l'implémentation héréditaire et je comprends qu'aucune surcharge de création d'objet.
EDIT : Mais vous auriez besoin d'une usine si vous créez l'objet de l'extérieur

Qu'est-ce qui rend la classe virtuelle pure moins désirable que l'idiome pimpl?

Arkaitz Jimenez
la source
3
Excellente question, je voulais juste demander la même chose. Voir aussi boost.org/doc/libs/1_41_0/libs/smart_ptr/sp_techniques.html
Frank

Réponses:

60

Lors de l'écriture d'une classe C ++, il convient de se demander si elle va être

  1. Un type de valeur

    Copie par valeur, l'identité n'a jamais d'importance. Il est approprié que ce soit une clé dans un std :: map. Exemple, une classe "chaîne", ou une classe "date", ou une classe "nombre complexe". "Copier" des instances d'une telle classe a du sens.

  2. Un type d'entité

    L'identité est importante. Toujours passé par référence, jamais par "valeur". Souvent, cela n'a aucun sens de "copier" des instances de la classe. Quand cela a du sens, une méthode polymorphe "Clone" est généralement plus appropriée. Exemples: une classe Socket, une classe Database, une classe "policy", tout ce qui serait une "fermeture" dans un langage fonctionnel.

PImpl et la classe de base abstraite pure sont des techniques pour réduire les dépendances de temps de compilation.

Cependant, je n'utilise pImpl que pour implémenter des types Value (type 1), et seulement parfois lorsque je veux vraiment minimiser les dépendances de couplage et de compilation. Souvent, cela ne vaut pas la peine. Comme vous le faites remarquer à juste titre, il y a plus de surcharge syntaxique car vous devez écrire des méthodes de transfert pour toutes les méthodes publiques. Pour les classes de type 2, j'utilise toujours une classe de base abstraite pure avec des méthodes de fabrique associées.

Paul Hollingsworth
la source
6
Veuillez consulter le commentaire de Paul de Vrieze sur cette réponse . Pimpl et Pure Virtual diffèrent considérablement si vous êtes dans une bibliothèque et que vous souhaitez échanger votre .so / .dll sans reconstruire le client. Les clients se lient aux interfaces pimpl par leur nom, il suffit donc de conserver les anciennes signatures de méthode. OTOH dans le cas purement abstrait, ils lient efficacement par index vtable, donc la réorganisation des méthodes ou l'insertion au milieu cassera la compatibilité.
SnakE
1
Vous pouvez uniquement ajouter (ou réorganiser) des méthodes dans un frontal de classe Pimpl pour maintenir la comparabilité binaire. Logiquement parlant, vous avez encore changé l'interface et cela semble un peu louche. La réponse ici est un équilibre judicieux qui peut également aider avec les tests unitaires via "injection de dépendances"; mais la réponse dépend toujours des exigences. Les rédacteurs de bibliothèque tiers (par opposition à l'utilisation d'une bibliothèque dans votre propre organisation) peuvent fortement préférer Pimpl.
Spacen Jasset
31

Pointer to implementationconsiste généralement à masquer les détails de mise en œuvre structurelle. Interfacesconcernent l'instanciation de différentes implémentations. Ils servent vraiment deux objectifs différents.

D.Shawley
la source
13
pas nécessairement, j'ai vu des classes qui stockent plusieurs pimpls en fonction de l'implémentation souhaitée. Souvent, cela signifie un impl win32 vs un impl linux de quelque chose qui doit être implémenté différemment par plate-forme.
Doug T.
14
Mais vous pouvez utiliser une interface pour découpler les détails d'implémentation et les masquer
Arkaitz Jimenez
6
Bien que vous puissiez implémenter pimpl à l'aide d'une interface, il n'y a souvent aucune raison de découpler les détails d'implémentation. Il n'y a donc aucune raison de devenir polymorphe. La raison de pimpl est de garder les détails d'implémentation loin du client (en C ++ pour les garder hors de l'en-tête). Vous pouvez le faire en utilisant une base / interface abstraite, mais en général, c'est une surpuissance inutile.
Michael Burr
10
Pourquoi est-ce exagéré? Je veux dire, est-ce que la méthode d'interface est plus lente que celle de pimpl? Il peut y avoir des raisons logiques, mais d'un point de vue pratique, je dirais qu'il est plus facile de le faire avec une interface abstraite
Arkaitz Jimenez
1
Je dirais que la classe / interface de base abstraite est la manière «normale» de faire les choses et permet des tests plus faciles via moquerie
paulm
28

L'idiome pimpl vous aide à réduire les dépendances et les temps de construction, en particulier dans les grandes applications, et minimise l'exposition de l'en-tête des détails d'implémentation de votre classe à une unité de compilation. Les utilisateurs de votre classe ne devraient même pas avoir besoin d'être conscients de l'existence d'un bouton (sauf en tant que pointeur cryptique dont ils ne sont pas au courant!).

Les classes abstraites (virtuels purs) sont quelque chose dont vos clients doivent être conscients: si vous essayez de les utiliser pour réduire le couplage et les références circulaires, vous devez ajouter un moyen de leur permettre de créer vos objets (par exemple via des méthodes d'usine ou des classes, injection de dépendance ou autres mécanismes).

Pontus Gagge
la source
17

Je cherchais une réponse pour la même question. Après avoir lu quelques articles et un peu de pratique, je préfère utiliser des "interfaces de classe virtuelle pures" .

  1. Ils sont plus simples (c'est une opinion subjective). L'idiome de Pimpl me fait sentir que j'écris du code "pour le compilateur", pas pour le "prochain développeur" qui lira mon code.
  2. Certains frameworks de test prennent en charge directement les classes virtuelles pures Mocking
  3. Il est vrai qu'il faut une usine pour être accessible de l'extérieur. Mais si vous voulez tirer parti du polymorphisme: c'est aussi "pro", pas un "con". ... et une méthode d'usine simple ne fait pas trop mal

Le seul inconvénient ( j'essaie d'enquêter à ce sujet ) est que l'idiome pimpl pourrait être plus rapide

  1. lorsque les appels proxy sont en ligne, alors que l'héritage nécessite nécessairement un accès supplémentaire à l'objet VTABLE au moment de l'exécution
  2. l'empreinte mémoire de la classe de proxy public pimpl est plus petite (vous pouvez facilement faire des optimisations pour des échanges plus rapides et d'autres optimisations similaires)
Ilias Bartolini
la source
21
Souvenez-vous également qu'en utilisant l'héritage, vous introduisez une dépendance sur la disposition de la vtable. Pour maintenir ABI, vous ne pouvez plus changer les fonctions virtuelles (l'ajout à la fin est en quelque sorte sûr, s'il n'y a pas de classes enfants qui ajoutent leurs propres méthodes virtuelles).
Paul de Vrieze
1
^ Ce commentaire ici devrait être collant.
CodeAngry
10

Je déteste les boutons! Ils font la classe moche et illisible. Toutes les méthodes sont redirigées vers Pimple. Vous ne voyez jamais dans les en-têtes, quelles fonctionnalités a la classe, donc vous ne pouvez pas la refactoriser (par exemple changer simplement la visibilité d'une méthode). La classe se sent comme "enceinte". Je pense que l'utilisation d'iterfaces est meilleure et vraiment suffisante pour cacher l'implémentation au client. Vous pouvez éventuellement laisser une classe implémenter plusieurs interfaces pour les maintenir minces. Il faut préférer les interfaces! Remarque: vous n'avez pas nécessairement besoin de la classe d'usine. Il est important que les clients de classe communiquent avec ses instances via l'interface appropriée. La dissimulation de méthodes privées me semble une étrange paranoïa et je ne vois pas de raison à cela puisque nous avons des interfaces.

Masha Ananieva
la source
1
Dans certains cas, vous ne pouvez pas utiliser d'interfaces virtuelles pures. Par exemple, lorsque vous avez du code hérité et que vous avez deux modules que vous devez séparer sans les toucher.
AlexTheo
Comme @Paul de Vrieze l'a souligné ci-dessous, vous perdez la compatibilité ABI en changeant les méthodes de la classe de base, car vous avez une dépendance implicite sur la vtable de la classe. Cela dépend du cas d'utilisation, s'il s'agit d'un problème.
H.Rittich
"Le masquage des méthodes privées que je trouve comme une étrange paranoïa" Cela ne vous permet-il pas de masquer les dépendances et donc de minimiser le temps de compilation si une dépendance change?
pooya13
Je ne comprends pas non plus comment les usines sont plus faciles à re-factoriser que pImpl. Ne quittez-vous pas "l'interface" dans les deux cas et ne changez pas l'implémentation? Dans Factory, vous devez modifier un fichier .h et un .cpp et dans pImpl vous devez modifier un .h et deux fichiers .cpp mais c'est à peu près tout et vous n'avez généralement pas besoin de modifier le fichier cpp de l'interface de pImpl.
pooya13
8

Il y a un problème très réel avec les bibliothèques partagées que l'idiome pimpl contourne parfaitement que les virtuels purs ne peuvent pas: vous ne pouvez pas modifier / supprimer en toute sécurité les membres de données d'une classe sans forcer les utilisateurs de la classe à recompiler leur code. Cela peut être acceptable dans certaines circonstances, mais pas par exemple pour les bibliothèques système.

Pour expliquer le problème en détail, considérez le code suivant dans votre bibliothèque / en-tête partagé:

// header
struct A
{
public:
  A();
  // more public interface, some of which uses the int below
private:
  int a;
};

// library 
A::A()
  : a(0)
{}

Le compilateur émet du code dans la bibliothèque partagée qui calcule l'adresse de l'entier à initialiser pour être un certain décalage (probablement zéro dans ce cas, car c'est le seul membre) du pointeur vers l'objet A qu'il sait être this.

Du côté utilisateur du code, a new Aallouera d'abord des sizeof(A)octets de mémoire, puis remettra un pointeur vers cette mémoire au A::A()constructeur sous la forme this.

Si dans une révision ultérieure de votre bibliothèque vous décidez de supprimer l'entier, de le rendre plus grand, plus petit ou d'ajouter des membres, il y aura un décalage entre la quantité de mémoire allouée par le code de l'utilisateur et les décalages attendus par le code du constructeur. Le résultat probable est un crash, si vous avez de la chance - si vous êtes moins chanceux, votre logiciel se comporte bizarrement.

En pimpl'ing, vous pouvez ajouter et supprimer en toute sécurité des membres de données à la classe interne, car l'allocation de mémoire et l'appel du constructeur se produisent dans la bibliothèque partagée:

// header
struct A
{
public:
  A();
  // more public interface, all of which delegates to the impl
private:
  void * impl;
};

// library 
A::A()
  : impl(new A_impl())
{}

Tout ce que vous avez à faire maintenant est de garder votre interface publique exempte de membres de données autres que le pointeur vers l'objet d'implémentation, et vous êtes à l'abri de cette classe d'erreurs.

Edit: Je devrais peut-être ajouter que la seule raison pour laquelle je parle du constructeur ici est que je ne voulais pas fournir plus de code - la même argumentation s'applique à toutes les fonctions qui accèdent aux données membres.


la source
4
Au lieu de void *, je pense qu'il est plus traditionnel de déclarer en avant la classe d'implémentation:class A_impl *impl_;
Frank Krueger
9
Je ne comprends pas, vous ne devriez pas déclarer de membres privés dans une classe pure virtuelle que vous avez l'intention d'utiliser comme interface, l'idée est de garder la classe pruement abstraite, pas de taille, seulement des méthodes virtuelles pures, je ne vois rien vous ne pouvez pas faire via les bibliothèques partagées
Arkaitz Jimenez
@Frank Krueger: Vous avez raison, j'étais paresseux. @Arkaitz Jimenez: Léger malentendu; si vous avez une classe qui ne contient que des fonctions virtuelles pures, alors il ne sert à rien de parler de bibliothèques partagées. D'un autre côté, si vous avez affaire à des bibliothèques partagées, pimpl'ing vos classes publiques peut être prudent pour la raison exposée ci-dessus.
10
C'est tout simplement incorrect. Les deux méthodes vous permettent de masquer l'état d'implémentation de vos classes, si vous faites de votre autre classe une classe "pure base abstraite".
Paul Hollingsworth
10
La première phrase de votre anser implique que les virtuels purs avec une méthode de fabrique associée ne vous permettent pas de cacher l'état interne de la classe. Ce n'est pas vrai. Les deux techniques vous permettent de masquer l'état interne de la classe. La différence réside dans son apparence pour l'utilisateur. pImpl vous permet de toujours représenter une classe avec une sémantique de valeur tout en masquant également l'état interne. La méthode d'usine Pure Abstract Base Class + vous permet de représenter les types d'entités et vous permet également de masquer l'état interne. Ce dernier est exactement comment fonctionne COM. Le chapitre 1 de "Essential COM" a une grande discussion à ce sujet.
Paul Hollingsworth
6

Nous ne devons pas oublier que l'héritage est un couplage plus fort et plus étroit que la délégation. Je prendrais également en compte toutes les questions soulevées dans les réponses données au moment de décider quels idiomes de conception utiliser pour résoudre un problème particulier.

Sam
la source
3

Bien que largement couvert dans les autres réponses, je peux peut-être être un peu plus explicite sur un avantage de pimpl par rapport aux classes de base virtuelles:

Une approche pimpl est transparente du point de vue de l'utilisateur, ce qui signifie que vous pouvez par exemple créer des objets de la classe sur la pile et les utiliser directement dans des conteneurs. Si vous essayez de masquer l'implémentation à l'aide d'une classe de base virtuelle abstraite, vous devrez renvoyer un pointeur partagé vers la classe de base à partir d'une fabrique, ce qui complique son utilisation. Considérez le code client équivalent suivant:

// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();

std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
    std::cout << o.SomeFun1();

// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();

std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.push_back(ObjectABC::CreateObject(13));
objs2.push_back(ObjectABC::CreateObject(14));
objs2.push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
    std::cout << o->SomeFun1();
Superfly Jon
la source
2

À ma connaissance, ces deux choses servent des objectifs complètement différents. Le but de l'idiome Pimple est essentiellement de vous donner une idée de votre implémentation afin que vous puissiez faire des choses comme des échanges rapides pour un tri.

Le but des classes virtuelles est plutôt de permettre le polymorphisme, c'est-à-dire que vous avez un pointeur inconnu vers un objet d'un type dérivé et lorsque vous appelez la fonction x, vous obtenez toujours la bonne fonction pour la classe sur laquelle le pointeur de base pointe réellement.

Des pommes et des oranges vraiment.

Robert S. Barnes
la source
Je suis d'accord avec les pommes / oranges. Mais il semble que vous utilisez pImpl pour une fonctionnelle. Mon objectif est principalement la technique de construction et la dissimulation d'informations.
xtofl
2

Le problème le plus ennuyeux de l'idiome pimpl est qu'il rend extrêmement difficile la maintenance et l'analyse du code existant. Donc, en utilisant pimpl, vous payez avec le temps et la frustration du développeur uniquement pour "réduire les dépendances et les temps de construction et minimiser l'exposition de l'en-tête des détails d'implémentation". Décidez-vous si cela en vaut vraiment la peine.

En particulier, les "temps de construction" sont un problème que vous pouvez résoudre par un meilleur matériel ou en utilisant des outils comme Incredibuild (www.incredibuild.com, également déjà inclus dans Visual Studio 2017), n'affectant ainsi pas la conception de votre logiciel. La conception du logiciel doit être généralement indépendante de la façon dont le logiciel est construit.

Trantor
la source
Vous payez également avec le temps du développeur lorsque les temps de construction sont de 20 minutes au lieu de 2, donc c'est un peu un équilibre, un vrai système de modules aiderait beaucoup ici.
Arkaitz Jimenez
À mon humble avis, la façon dont le logiciel est construit ne devrait pas du tout influencer la conception interne. C'est un problème complètement différent.
Trantor
2
Qu'est-ce qui rend l'analyse difficile? Un tas d'appels dans un fichier d'implémentation transféré à la classe Impl ne semble pas difficile.
mabraham
2
Imaginez des implémentations de débogage où pimpl et les interfaces sont tous deux utilisés. À partir d'un appel dans le code utilisateur A, vous tracez jusqu'à l'interface B, passez à la classe à picots C pour enfin commencer à déboguer la classe d'implémentation D ... Quatre étapes jusqu'à ce que vous puissiez analyser ce qui se passe réellement. Et si le tout est implémenté dans une DLL, vous trouverez éventuellement une interface C quelque part entre ....
Trantor le
Pourquoi utiliseriez-vous une interface avec pImpl alors que pImpl peut également faire le travail d'une interface? (c'est-à-dire qu'il peut vous aider à atteindre l'inversion de dépendance)
pooya13