175 votes

L'idiome PIMPL est-il réellement utilisé dans la pratique ?

Je suis en train de lire le livre "Un C++ exceptionnel" par Herb Sutter, et dans ce livre j'ai appris l'idiome PIMPL. En gros, l'idée est de créer une structure de private les objets d'une class et les allouer dynamiquement à diminuer le temps de compilation (et aussi cacher les implémentations privées d'une meilleure manière).

Par exemple :

class X
{
private:
  C c;
  D d;
} ;

pourrait être changé en :

class X
{
private:
  struct XImpl;
  XImpl* pImpl;
};

et, dans le fichier .cpp, la définition :

struct X::XImpl
{
  C c;
  D d;
};

Cela semble assez intéressant, mais je n'ai jamais vu ce genre d'approche auparavant, ni dans les entreprises où j'ai travaillé, ni dans les projets open source dont j'ai vu le code source. Je me demande donc si cette technique est réellement utilisée dans la pratique ?

Dois-je l'utiliser partout, ou avec prudence ? Et cette technique est-elle recommandée pour être utilisée dans les systèmes embarqués (où les performances sont très importantes) ?

0 votes

Est-ce essentiellement la même chose que de décider que X est une interface (abstraite) et que Ximpl est l'implémentation ? struct XImpl : public X . Cela me semble plus naturel. Y a-t-il un autre problème que j'ai manqué ?

0 votes

@AaronMcDaid : C'est similaire, mais a les avantages que (a) les fonctions membres n'ont pas besoin d'être virtuelles, et (b) vous n'avez pas besoin d'une fabrique, ou de la définition de la classe d'implémentation, pour l'instancier.

3 votes

@AaronMcDaid L'idiome pimpl évite les appels de fonctions virtuelles. C'est aussi un peu plus proche de C++ (pour une certaine conception de C++) ; vous invoquez les constructeurs, plutôt que les fonctions de fabrique. J'ai utilisé les deux, en fonction de ce qui se trouve dans la base de code existante - l'idiome pimpl (appelé à l'origine l'idiome du chat de Cheshire, et précédant d'au moins 5 ans la description qu'en fait Herb) semble avoir une histoire plus longue et être plus largement utilisé en C++, mais sinon, les deux fonctionnent.

142voto

BЈовић Points 28674

Je me demande donc si cette technique est réellement utilisée dans la pratique ? Dois-je l'utiliser partout, ou avec prudence ?

Bien sûr qu'il est utilisé. Je l'utilise dans mon projet, dans presque tous les cours.


Raisons d'utiliser l'idiome PIMPL :

Compatibilité binaire

Lorsque vous développez une bibliothèque, vous pouvez ajouter/modifier des champs dans la bibliothèque. XImpl sans rompre la compatibilité binaire avec votre client (ce qui signifierait des plantages !). Puisque la disposition binaire de X ne change pas lorsque vous ajoutez de nouveaux champs à la classe Ximpl il est possible d'ajouter de nouvelles fonctionnalités à la bibliothèque dans les mises à jour mineures des versions.

Bien entendu, vous pouvez également ajouter de nouvelles méthodes non virtuelles publiques/privées à l'interface de l'UE. X / XImpl sans rompre la compatibilité binaire, mais cela correspond à la technique standard d'en-tête/implémentation.

Masquage des données

Si vous développez une bibliothèque, en particulier une bibliothèque propriétaire, il peut être souhaitable de ne pas divulguer quelles autres bibliothèques / techniques de mise en œuvre ont été utilisées pour mettre en œuvre l'interface publique de votre bibliothèque. Soit pour des questions de propriété intellectuelle, soit parce que vous pensez que les utilisateurs pourraient être tentés de faire des suppositions dangereuses sur l'implémentation ou de briser l'encapsulation en utilisant de terribles astuces de casting. PIMPL résout/atténue ce problème.

Temps de compilation

Le temps de compilation est réduit, puisque seul le fichier source (d'implémentation) de X doit être reconstruite lorsque vous ajoutez/supprimez des champs et/ou des méthodes à l'application XImpl (ce qui revient à ajouter des champs/méthodes privés selon la technique standard). En pratique, il s'agit d'une opération courante.

Avec la technique standard d'en-tête/implémentation (sans PIMPL), lorsque vous ajoutez un nouveau champ à l'élément X chaque client qui alloue X (soit sur la pile, soit sur le tas) doit être recompilé, car il doit ajuster la taille de l'allocation. Eh bien, chaque client qui n'alloue jamais X également doivent être recompilés, mais ce n'est qu'une surcharge (le code résultant du côté client sera le même).

De plus, avec la séparation standard en-tête/implémentation XClient1.cpp doit être recompilé même lorsqu'une méthode privée X::foo() a été ajouté à X y X.h a changé, même si XClient1.cpp ne peut pas appeler cette méthode pour des raisons d'encapsulation ! Comme ci-dessus, il s'agit d'une pure surcharge et elle est liée à la façon dont les systèmes de construction C++ fonctionnent dans la vie réelle.

Bien sûr, la recompilation n'est pas nécessaire lorsque l'on se contente de modifier l'implémentation des méthodes (car on ne touche pas à l'en-tête), mais cela correspond à la technique standard de l'en-tête/implémentation.


Cette technique est-elle recommandée pour être utilisée dans les systèmes embarqués (où les performances sont très importantes) ?

Cela dépend de la puissance de votre cible. Cependant, la seule réponse à cette question est : mesurez et évaluez ce que vous gagnez et perdez. Prenez également en compte le fait que si vous ne publiez pas une bibliothèque destinée à être utilisée dans des systèmes embarqués par vos clients, seul l'avantage du temps de compilation s'applique !

9 votes

Également, la compatibilité binaire

10 votes

Dans la bibliothèque Qt, cette méthode est également utilisée dans les situations de pointeur intelligent. Ainsi, QString conserve son contenu comme une classe immuable en interne. Lorsque la classe publique est "copiée", le pointeur du membre privé est copié au lieu de la classe privée entière. Ces classes privées utilisent alors également des pointeurs intelligents, de sorte que vous bénéficiez d'un ramassage des déchets avec la plupart des classes, en plus d'une performance grandement améliorée en raison de la copie du pointeur au lieu de la copie de la classe complète.

8 votes

De plus, avec l'idiome pimpl, Qt peut maintenir la compatibilité binaire avant et arrière dans une seule version majeure (dans la plupart des cas). C'est de loin la raison la plus importante pour l'utiliser.

52voto

PlasmaHH Points 8426

Il semble qu'un grand nombre de bibliothèques l'utilisent pour maintenir la stabilité de leur API, du moins pour certaines versions.

Mais comme pour toute chose, il ne faut jamais utiliser n'importe quoi partout sans précaution. Réfléchissez toujours avant de l'utiliser. Évaluez les avantages qu'il vous procure et voyez s'ils valent le prix que vous payez.

Les avantages qu'elle offre puede vous donner :

  • aide à maintenir la compatibilité binaire des bibliothèques partagées
  • cacher certains détails internes
  • diminution des cycles de recompilation

Il peut s'agir ou non d'avantages réels pour vous. Pour ma part, je ne me soucie pas de quelques minutes de recompilation. Les utilisateurs finaux ne s'en soucient généralement pas non plus, car ils compilent toujours une fois et depuis le début.

Les inconvénients possibles sont (également ici, en fonction de l'implémentation et s'il s'agit d'inconvénients réels pour vous) :

  • Augmentation de l'utilisation de la mémoire en raison d'un plus grand nombre d'allocations qu'avec la variante naïve.
  • effort de maintenance accru (vous devez écrire au moins les fonctions de transfert)
  • perte de performance (le compilateur peut ne pas être capable d'inliner des choses comme il le fait avec une implémentation naïve de votre classe)

Donnez donc soigneusement une valeur à chaque chose, et évaluez-la par vous-même. Pour moi, il s'avère presque toujours que l'utilisation de l'idiome PIMPL ne vaut pas la peine. Il n'y a qu'un seul cas où je l'utilise personnellement (ou du moins quelque chose de similaire) :

Mon enveloppe C++ pour le système Linux stat appel. Ici, la structure de l'en-tête C peut être différente, en fonction de ce que l'on veut faire. #defines sont définis. Et comme l'en-tête de mon wrapper ne peut pas tous les contrôler, j'ai seulement #include <sys/stat.h> dans mon .cxx et éviter ces problèmes.

2 votes

Il devrait presque toujours être utilisé pour les interfaces système, afin de rendre le code de l'interface indépendant du système. Mon site File (qui expose une grande partie des informations stat retournerait sous Unix) utilise la même interface sous Windows et Unix, par exemple.

6 votes

@JamesKanze : Même là, personnellement, je m'assiérais d'abord un moment et je me demanderais s'il n'est pas suffisant d'avoir quelques #ifdef pour que l'enveloppe soit aussi fine que possible. Mais chacun a des objectifs différents, l'important est de prendre le temps de réfléchir au lieu de suivre aveuglément quelque chose.

33voto

Emilio Garavaglia Points 9189

Je suis d'accord avec tous les autres au sujet des marchandises, mais permettez-moi de mettre en évidence une limite : ne fonctionne pas bien avec les modèles .

La raison en est que l'instanciation des modèles nécessite la déclaration complète disponible à l'endroit où l'instanciation a eu lieu. (Et c'est la raison principale pour laquelle vous ne voyez pas de méthodes de modèles définies dans des fichiers .cpp).

Vous pouvez toujours vous référer aux sous-classes modélisées, mais comme vous devez toutes les inclure, tous les avantages du "découplage de l'implémentation" lors de la compilation (éviter d'inclure tout le code spécifique à la plate-forme partout, raccourcir la compilation) sont perdus.

C'est un bon paradigme pour le classique OOP (basée sur l'héritage), mais pas pour la programmation générique (basée sur la spécialisation).

4 votes

Il faut être plus précis : il n'y a absolument aucun problème lorsque en utilisant Classes PIMPL comme arguments de type de modèle. Ce n'est que si la classe d'implémentation elle-même a besoin d'être paramétrée dans les arguments de type de la classe externe qu'elle ne peut plus être cachée dans l'en-tête de l'interface, même s'il s'agit toujours d'une classe privée. Si vous pouvez supprimer l'argument de modèle, vous pouvez certainement continuer à faire du PIMPL "correct". Avec la suppression du type, vous pouvez également faire le PIMPL dans une classe de base non modèle, et ensuite faire dériver la classe modèle de celle-ci.

25voto

Tibo Points 1323

D'autres personnes ont déjà présenté les avantages et inconvénients techniques, mais je pense que les points suivants méritent d'être soulignés :

D'abord et avant tout, ne soyez pas dogmatique. Si PIMPL fonctionne dans votre situation, utilisez-le - ne l'utilisez pas simplement parce que "c'est mieux OO puisqu'il vraiment cache l'implémentation", etc. Citation de la FAQ C++ :

l'encapsulation est pour le code, pas pour les gens ( source )

Pour vous donner un exemple de logiciel open source, où il est utilisé et pourquoi : OpenThreads, la bibliothèque de threading utilisée par le OpenSceneGraph . L'idée principale est de supprimer de l'en-tête (par exemple, <Thread.h> ) tout le code spécifique à la plate-forme, car les variables d'état internes (par exemple, les gestionnaires de threads) diffèrent d'une plate-forme à l'autre. De cette façon, on peut compiler du code contre votre bibliothèque sans connaître les particularités des autres plateformes, car tout est caché.

12voto

Ghita Points 1480

Je considérerais principalement le PIMPL pour les classes exposées pour être utilisées comme une API par d'autres modules. Cela présente de nombreux avantages, car la recompilation des modifications apportées à l'implémentation PIMPL n'affecte pas le reste du projet. De plus, pour les classes API, elles favorisent une compatibilité binaire (les changements dans l'implémentation d'un module n'affectent pas les clients de ces modules, ils n'ont pas besoin d'être recompilés car la nouvelle implémentation a la même interface binaire - l'interface exposée par le PIMPL).

Quant à l'utilisation de PIMPL pour chaque classe, je serais prudent car tous ces avantages ont un coût : un niveau supplémentaire d'indirection est nécessaire pour accéder aux méthodes d'implémentation.

0 votes

"un niveau supplémentaire d'indirection est nécessaire pour accéder aux méthodes d'implémentation." C'est le cas ?

2 votes

@xaxxon oui, il l'est. pimpl est plus lent si les méthodes sont de bas niveau. ne l'utilisez jamais pour des choses qui vivent dans une boucle serrée, par exemple.

1 votes

@xaxxon Je dirais que dans le cas général un niveau supplémentaire est nécessaire. Si l'inlining est effectué, alors non. Mais l'inlining ne serait pas une option dans le code compilé dans une dll différente.

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