L'idiome pImpl est-il vraiment utilisé dans la pratique?

165

Je lis le livre "Exceptional C ++" par Herb Sutter, et dans ce livre j'ai appris l'idiome pImpl. Fondamentalement, l'idée est de créer une structure pour les privateobjets de a classet de les allouer dynamiquement pour diminuer le temps de compilation (et aussi masquer 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 RPC, 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. Alors, je me demande si cette technique est vraiment 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)?

Renan Greinert
la source
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é?
Aaron McDaid
@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.
Mike Seymour
2
@AaronMcDaid L'idiome pimpl évite les appels de fonctions virtuelles. C'est aussi un peu plus C ++ - ish (pour une certaine conception de C ++ - ish); vous invoquez des constructeurs plutôt que des fonctions d'usine. J'ai utilisé les deux, en fonction de ce qui se trouve dans la base de code existante - l'idiome pimpl (à l'origine appelé l'idiome du chat Cheshire, et avant la description de Herb d'au moins 5 ans) semble avoir une histoire plus longue et être plus largement utilisé en C ++, mais sinon, les deux fonctionnent.
James Kanze
30
En C ++, pimpl doit être implémenté avec const unique_ptr<XImpl>plutôt que XImpl*.
Neil G
1
"jamais vu ce genre d'approche auparavant, ni dans les entreprises où j'ai travaillé, ni dans des projets open source". Qt ne l'utilise presque jamais.
ManuelSchneid3r

Réponses:

132

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

Bien sûr, il est utilisé. Je l'utilise dans mon projet, dans presque toutes les classes.


Raisons d'utiliser l'idiome PIMPL:

Compatibilité binaire

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

Bien sûr, vous pouvez également ajouter de nouvelles méthodes non virtuelles publiques / privées à X / XImplsans rompre la compatibilité binaire, mais c'est à égalité avec la technique d'en-tête / d'implémentation standard.

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 d'implémentation ont été utilisées pour implémenter l'interface publique de votre bibliothèque. Soit à cause de problèmes de propriété intellectuelle, soit parce que vous pensez que les utilisateurs pourraient être tentés de prendre des hypothèses dangereuses sur l'implémentation ou simplement casser l'encapsulation en utilisant de terribles astuces de casting. PIMPL résout / atténue cela.

Temps de compilation

Le temps de compilation est réduit, car seul le fichier source (implémentation) de Xdoit être reconstruit lorsque vous ajoutez / supprimez des champs et / ou des méthodes auXImpl classe (ce qui correspond à l'ajout de champs / méthodes privés dans la technique standard). En pratique, c'est une opération courante.

Avec la technique d'en-tête / d'implémentation standard (sans PIMPL), lorsque vous ajoutez un nouveau champ à X, chaque client qui alloue X(soit sur pile, soit sur tas) doit être recompilé, car il doit ajuster la taille de l'allocation. Eh bien, chaque client qui n'alloue jamais X doit également être recompilé, mais c'est juste 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, il XClient1.cppfaut recompiler même lorsqu'une méthode privée a X::foo()été ajoutée Xet X.hmodifiée, même si elle XClient1.cppne peut pas appeler cette méthode pour des raisons d'encapsulation! Comme ci-dessus, il s'agit d'une surcharge pure et liée au fonctionnement des systèmes de construction C ++ réels.

Bien sûr, la recompilation n'est pas nécessaire lorsque vous modifiez simplement l'implémentation des méthodes (car vous ne touchez pas l'en-tête), mais c'est à égalité avec la technique d'en-tête / d'implémentation standard.


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. De plus, sachez 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!

BЈовић
la source
16
+1 car il est largement utilisé dans l'entreprise pour laquelle je travaille également, et pour les mêmes raisons.
Benoit
9
aussi, compatibilité binaire
Ambroz Bizjak
9
Dans la bibliothèque Qt, cette méthode est également utilisée dans les situations de pointeur intelligent. Ainsi QString conserve son contenu en tant que classe immuable en interne. Lorsque la classe publique est "copiée", le pointeur du membre privé est copié à la place de la classe privée entière. Ces classes privées utilisent également des pointeurs intelligents, de sorte que vous obtenez essentiellement un ramassage des ordures avec la plupart des classes, en plus de performances considérablement améliorées grâce à la copie de pointeur au lieu de la copie de classe complète
Timothy Baldridge
8
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). OMI, c'est de loin la raison la plus importante pour l'utiliser.
whitequark
1
Il est également utile pour implémenter du code spécifique à la plate-forme, car vous pouvez conserver la même API.
doc
49

Il semble que de nombreuses bibliothèques l'utilisent pour rester stable dans leur API, du moins pour certaines versions.

Mais comme pour toutes choses, vous ne devriez jamais utiliser quoi que ce soit partout sans prudence. Réfléchissez toujours avant de l'utiliser. Évaluez les avantages que cela vous procure et s'ils valent le prix que vous payez.

Les avantages que cela peut vous apporter sont:

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

Ceux-ci peuvent être ou non de réels avantages pour vous. Comme pour moi, je me fiche de quelques minutes de recompilation. Les utilisateurs finaux ne le font généralement pas non plus, car ils le compilent toujours une fois et depuis le début.

Les inconvénients possibles sont (également ici, selon l'implémentation et si ce sont de réels inconvénients pour vous):

  • Augmentation de l'utilisation de la mémoire en raison de plus d'allocations qu'avec la variante naïve
  • un effort de maintenance accru (vous devez écrire au moins les fonctions de transfert)
  • perte de performances (le compilateur peut ne pas être en mesure d'insérer des éléments comme avec une implémentation naïve de votre classe)

Alors, donnez à tout une valeur 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 au moins quelque chose de similaire):

Mon wrapper C ++ pour l' statappel linux . Ici, la structure de l'en-tête C peut être différente, en fonction de ce qui #definesest défini. Et comme mon en-tête wrapper ne peut pas les contrôler tous, je ne les ai que #include <sys/stat.h>dans mon .cxxfichier et évite ces problèmes.

PlasmaHH
la source
2
Il devrait presque toujours être utilisé pour les interfaces système, pour rendre le système de code d'interface indépendant. Ma Fileclasse (qui expose une grande partie des informations statrenvoyées sous Unix) utilise la même interface sous Windows et Unix, par exemple.
James Kanze
5
@JamesKanze: Même là, personnellement, je m'assois d'abord un moment et je me demande s'il ne suffit peut-être pas d'avoir quelques #ifdefsecondes pour rendre l'emballage aussi fin que possible. Mais chacun a des objectifs différents, l'important est de prendre le temps d'y réfléchir au lieu de suivre aveuglément quelque chose.
PlasmaHH
31

D'accord avec tous les autres sur les produits, mais permettez-moi de mettre en évidence une limite: ne fonctionne pas bien avec les modèles .

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

Vous pouvez toujours faire référence à des sous-classes modélisées, mais comme vous devez toutes les inclure, tous les avantages du «découplage d'implémentation» lors de la compilation (en évitant d'inclure tout le code spécifique à la plate-forme partout, en raccourcissant la compilation) sont perdus.

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

Emilio Garavaglia
la source
4
Il faut être plus précis: il n'y a absolument aucun problème lors de l' utilisation des classes PIMPL comme arguments de type template. Ce n'est que si la classe d'implémentation elle-même doit être paramétrée sur les arguments de modèle de la classe externe, elle ne peut plus être cachée de l'en-tête de l'interface, même si c'est toujours une classe privée. Si vous pouvez supprimer l'argument de modèle, vous pouvez certainement toujours faire un PIMPL "correct". Avec la suppression de type, vous pouvez également effectuer le PIMPL dans une classe de base non-modèle, puis faire en dériver la classe de modèle.
Réintégrer Monica
22

D'autres personnes ont déjà fourni les avantages / inconvénients techniques, mais je pense que ce qui suit mérite d'être noté:

Avant tout, ne soyez pas dogmatique. Si pImpl fonctionne pour votre situation, utilisez-le - ne l'utilisez pas simplement parce que "c'est mieux OO car il cache vraiment l' implémentation" etc. Citant la FAQ C ++:

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

Juste pour vous donner un exemple de logiciel open source où il est utilisé et pourquoi: OpenThreads, la bibliothèque de threads utilisée par 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 poignées de thread) diffèrent d'une plate-forme à l'autre. De cette façon, on peut compiler du code contre votre bibliothèque sans aucune connaissance des particularités des autres plates-formes, car tout est caché.

azalée
la source
12

Je considérerais principalement PIMPL pour les classes exposées pour être utilisées comme 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 d'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).

En ce qui concerne l'utilisation de PIMPL pour chaque classe, je considérerais la prudence 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.

Ghita
la source
"un niveau supplémentaire d'indirection est nécessaire pour accéder aux méthodes d'implémentation." C'est?
xaxxon
@xaxxon oui, ça 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.
Erik Aronesty
@xaxxon Je dirais qu'en général, un niveau supplémentaire est requis. Si l'inlining est effectué, non. Mais l'inlinning ne serait pas une option dans le code compilé dans une autre DLL.
Ghita
5

Je pense que c'est l'un des outils les plus fondamentaux pour le découplage.

J'utilisais pimpl (et de nombreux autres idiomes d'Exceptional C ++) sur un projet intégré (SetTopBox).

Le but particulier de cet idoim dans notre projet était de masquer les types utilisés par la classe XImpl. Plus précisément, nous l'avons utilisé pour masquer les détails des implémentations pour différents matériels, où différents en-têtes seraient extraits. Nous avions différentes implémentations de classes XImpl pour une plate-forme et différentes pour l'autre. La disposition de la classe X est restée la même quelle que soit la plate-forme.

utilisateur377178
la source
4

J'utilisais beaucoup cette technique dans le passé, mais je me suis ensuite retrouvé à m'en éloigner.

Bien sûr, il est judicieux de cacher les détails de l'implémentation aux utilisateurs de votre classe. Cependant, vous pouvez également le faire en faisant en sorte que les utilisateurs de la classe utilisent une interface abstraite et que le détail de l'implémentation soit la classe concrète.

Les avantages de pImpl sont:

  1. En supposant qu'il n'y ait qu'une seule implémentation de cette interface, il est plus clair de ne pas utiliser de classe abstraite / implémentation concrète

  2. Si vous avez une suite de classes (un module) telle que plusieurs classes accèdent au même "impl" mais que les utilisateurs du module n'utiliseront que les classes "exposées".

  3. Pas de v-table si cela est supposé être une mauvaise chose.

Les inconvénients que j'ai trouvés de pImpl (où l'interface abstraite fonctionne mieux)

  1. Bien que vous n'ayez qu'une seule implémentation «production», en utilisant une interface abstraite, vous pouvez également créer une implémentation «fictive» qui fonctionne dans les tests unitaires.

  2. (Le plus gros problème). Avant les jours de unique_ptr et de déménagement, vous aviez des choix restreints quant à la façon de stocker le pImpl. Un pointeur brut et vous avez eu des problèmes avec votre classe non copiable. Un ancien auto_ptr ne fonctionnerait pas avec une classe déclarée avant (pas sur tous les compilateurs de toute façon). Les gens ont donc commencé à utiliser shared_ptr, ce qui était bien pour rendre votre classe copiable, mais bien sûr, les deux copies avaient le même shared_ptr sous-jacent auquel vous ne vous attendiez peut-être pas (modifiez-en une et les deux sont modifiées). Ainsi, la solution était souvent d'utiliser le pointeur brut pour le pointeur interne et de rendre la classe non copiable et de renvoyer un shared_ptr à la place. Donc deux appels à nouveau. (En fait, 3 étant donné l'ancien shared_ptr vous en ont donné un deuxième).

  3. Techniquement pas vraiment correct car la constness n'est pas propagée à un pointeur de membre.

En général, je me suis donc éloigné au cours des années de pImpl pour passer à l'utilisation d'interface abstraite (et aux méthodes d'usine pour créer des instances).

Vache à lait
la source
3

Comme beaucoup d'autres l'ont dit, l'idiome Pimpl permet d'atteindre une indépendance complète de dissimulation d'informations et de compilation, malheureusement avec le coût de la perte de performances (indirection de pointeur supplémentaire) et du besoin de mémoire supplémentaire (le pointeur de membre lui-même). Le coût supplémentaire peut être critique dans le développement de logiciels embarqués, en particulier dans les scénarios où la mémoire doit être économisée autant que possible. L'utilisation de classes abstraites C ++ comme interfaces conduirait aux mêmes avantages au même coût. Cela montre en fait une grande carence du C ++ où, sans se répéter aux interfaces de type C (méthodes globales avec un pointeur opaque comme paramètre), il n'est pas possible d'avoir une véritable indépendance de masquage et de compilation d'informations sans inconvénients de ressources supplémentaires: c'est principalement parce que le déclaration d'une classe, qui doit être incluse par ses utilisateurs,

ncsc
la source
3

Voici un scénario réel que j'ai rencontré, où cet idiome a beaucoup aidé. J'ai récemment décidé de prendre en charge DirectX 11, ainsi que mon support DirectX 9 existant, dans un moteur de jeu. Le moteur intégrait déjà la plupart des fonctionnalités DX, donc aucune des interfaces DX n'a ​​été utilisée directement; ils étaient simplement définis dans les en-têtes comme des membres privés. Le moteur utilise des DLL comme extensions, ajoutant la prise en charge du clavier, de la souris, du joystick et des scripts, comme semaine, comme de nombreuses autres extensions. Alors que la plupart de ces DLL n'utilisaient pas directement DX, elles nécessitaient des connaissances et un lien avec DX simplement parce qu'elles inséraient des en-têtes qui exposaient DX. En ajoutant DX 11, cette complexité allait augmenter considérablement, mais inutilement. Le déplacement des membres DX dans un Pimpl défini uniquement dans la source a éliminé cette imposition. En plus de cette réduction des dépendances de bibliothèque,

Kit10
la source
2

Il est utilisé en pratique dans de nombreux projets. Son utilité dépend fortement du type de projet. L'un des projets les plus importants qui l'utilisent est Qt , où l'idée de base est de masquer l'implémentation ou le code spécifique à la plate-forme à l'utilisateur (d'autres développeurs utilisant Qt).

C'est une idée noble mais il y a un réel inconvénient à cela: le débogage Tant que le code caché dans les implémentations privées est de qualité supérieure, tout va bien, mais s'il y a des bogues là-dedans, alors l'utilisateur / développeur a un problème, parce que c'est juste un pointeur stupide vers une implémentation cachée, même s'il a le code source des implémentations.

Donc, comme dans presque toutes les décisions de conception, il y a des avantages et des inconvénients.

Holger Kretzschmar
la source
9
c'est idiot mais c'est tapé ... pourquoi ne pouvez-vous pas suivre le code dans le débogueur?
UncleZeiv
2
De manière générale, pour déboguer dans le code Qt, vous devez créer vous-même Qt. Une fois que vous le faites, il n'y a aucun problème à entrer dans les méthodes PIMPL et à inspecter le contenu des données PIMPL.
Réintégrer Monica
0

Un avantage que je peux voir est qu'il permet au programmeur d'implémenter certaines opérations de manière assez rapide:

X( X && move_semantics_are_cool ) : pImpl(NULL) {
    this->swap(move_semantics_are_cool);
}
X& swap( X& rhs ) {
    std::swap( pImpl, rhs.pImpl );
    return *this;
}
X& operator=( X && move_semantics_are_cool ) {
    return this->swap(move_semantics_are_cool);
}
X& operator=( const X& rhs ) {
    X temporary_copy(rhs);
    return this->swap(temporary_copy);
}

PS: J'espère que je ne comprends pas mal la sémantique des mouvements.

BenGoldberg
la source