Je lis le livre "Exceptional C++" de Herb Sutter, et dans ce livre j'ai appris sur l'idiome pImpl. Fondamentalement, l'idée est de créer une structure pour les objets private
d'un class
et de les allouer dynamiquement à diminuer le temps de compilation (et également masquer les implémentations privées dans 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 sur lesquelles 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 en 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)?
Alors, je me demande si cette technique est vraiment utilisée en 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.
Lorsque vous développez une bibliothèque, vous pouvez ajouter/modifier des champs dans XImpl
sans rompre la compatibilité binaire avec votre client (ce qui signifierait des plantages!). Étant donné que la disposition binaire de la classe X
ne change pas lorsque vous ajoutez de nouveaux champs à la classe Ximpl
, il est sûr 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 publiques/privées non virtuelles à X
/XImpl
sans rompre la compatibilité binaire, mais c'est comparable à la technique d'en-tête/d'implémentation standard.
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 en raison de problèmes de propriété intellectuelle, soit parce que vous pensez que les utilisateurs peuvent être tentés de prendre des hypothèses dangereuses sur la mise en œuvre ou simplement de rompre l'encapsulation en utilisant de terribles astuces de casting. PIMPL résout/atténue cela.
Le temps de compilation est réduit, car seul le fichier source (implémentation) de X
doit être reconstruit lorsque vous ajoutez/supprimez des champs et/ou des méthodes à la classe XImpl
(qui correspond à l'ajout de champs privés/méthodes 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 jamais 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 aussi doit ê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, XClient1.cpp
Doit être recompilé même lorsqu'une méthode privée X::foo()
a été ajoutée à X
et X.h
Modifiée , même si XClient1.cpp
ne peut pas appeler cette méthode pour des raisons d'encapsulation! Comme ci-dessus, c'est une surcharge pure et est 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 (parce que vous ne touchez pas l'en-tête), mais c'est comparable à la technique d'en-tête/d'implémentation standard.
Est-il recommandé d'utiliser cette technique 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: mesurer et évaluer ce que vous gagnez et perdez. Prenez également en compte que si vous ne publiez pas une bibliothèque destinée à être utilisée dans les systèmes embarqués par vos clients, seul l'avantage de temps de compilation s'applique!
Il semble que de nombreuses bibliothèques l'utilisent pour rester stable dans leur API, au moins pour certaines versions.
Mais comme pour toutes choses, vous ne devez jamais rien utiliser partout sans prudence. Réfléchissez toujours avant de l'utiliser. Évaluez les avantages que cela vous offre et s'ils valent le prix que vous payez.
Les avantages que cela peut vous donner sont:
Ces avantages peuvent ou non être réels 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 la mise en œuvre et s'ils sont de réels inconvénients pour vous):
Alors, donnez à chaque chose 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'appel linux stat
. Ici, la structure de l'en-tête C peut être différente, selon ce que #defines
sont définis. Et comme mon en-tête wrapper ne peut pas les contrôler tous, je ne #include <sys/stat.h>
dans mon .cxx
fichier et éviter ces problèmes.
D'accord avec tous les autres sur les 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 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 de méthodes de modèle définies dans les 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 (en évitant d'inclure partout tout le code spécifique à platoform, raccourcissant la compilation) sont perdus.
Est un bon paradigme pour le classique OOP (basé sur l'héritage) mais pas pour la programmation générique (basé sur la spécialisation).
D'autres personnes ont déjà fourni les avantages/inconvénients techniques, mais je pense que ce qui suit mérite d'être noté:
D'abord et 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 vraiment masque 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 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 descripteurs de threads) diffèrent d'une plate-forme à l'autre. De cette façon, on peut compiler du code par rapport à votre bibliothèque sans aucune connaissance des particularités des autres plateformes, car tout est caché.
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 il rend la recompilation des modifications apportées dans la mise en œuvre PIMPL n'affecte pas le reste du projet. De plus, pour les classes d'API, elles favorisent une compatibilité binaire (les changements dans une implémentation de 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 considérerais la prudence car tous ces avantages ont un coût: un niveau d'indirection supplémentaire est requis pour accéder aux méthodes d'implémentation.
Je pense que c'est l'un des outils les plus fondamentaux du 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, dans lesquels 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 platine.
J'utilisais beaucoup cette technique dans le passé, mais je me suis alors éloigné d'elle.
Bien sûr, c'est une bonne idée de cacher les détails de l'implémentation aux utilisateurs de votre classe. Cependant, vous pouvez également le faire en amenant les utilisateurs de la classe à utiliser une interface abstraite et à ce que le détail de l'implémentation soit la classe concrète.
Les avantages de pImpl sont:
En supposant qu'il n'y a qu'une seule implémentation de cette interface, elle est plus claire en n'utilisant pas une implémentation de classe abstraite/concrète
Si vous avez une suite de classes (un module) telle que plusieurs classes accèdent au même "impl" mais les utilisateurs du module n'utiliseront que les classes "exposées".
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)
Bien que vous ne puissiez avoir qu'une seule implémentation de "production", en utilisant une interface abstraite, vous pouvez également créer une implémentation "factice" qui fonctionne dans les tests unitaires.
(Le plus gros problème). Avant les jours de unique_ptr et de déplacement, vous aviez des choix limités sur la façon de stocker le pImpl. Un pointeur brut et vous avez eu des problèmes de non-copie de votre classe. Un ancien auto_ptr ne fonctionnerait pas avec une classe déclarée vers l'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 pas (modifiez-en un et les deux sont modifiés). Ainsi, la solution était souvent d'utiliser un pointeur brut pour le pointeur interne et de rendre la classe non copiable et de renvoyer un shared_ptr à cela à la place. Donc, deux appels à nouveau. (En fait, 3 étant donné l'ancien shared_ptr vous en a donné un deuxième).
Techniquement pas vraiment const-correct car la constness n'est pas propagée à un pointeur membre.
En général, je me suis donc éloigné au cours des années de pImpl pour utiliser plutôt une interface abstraite (et des méthodes d'usine pour créer des instances).
Comme beaucoup d'autres l'ont dit, l'idiome Pimpl permet d'atteindre une indépendance complète de masquage et de compilation d'informations, 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++ en tant qu'interfaces entraînerait les mêmes avantages au même coût. Cela montre en fait une grande déficience de C++ où, sans revenir à des interfaces de type C (méthodes globales avec un pointeur opaque comme paramètre), il n'est pas possible d'avoir un véritable masquage d'informations et une indépendance de compilation sans inconvénients supplémentaires pour les ressources: c'est principalement parce que le La déclaration d'une classe, qui doit être incluse par ses utilisateurs, exporte non seulement l'interface de la classe (méthodes publiques) dont les utilisateurs ont besoin, mais aussi ses internes (membres privés), dont les utilisateurs n'ont pas besoin.
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 comprenait déjà la plupart des fonctionnalités DX, donc aucune des interfaces DX n'a été utilisée directement; ils étaient juste définis dans les en-têtes comme des membres privés. Le moteur utilise des DLL comme extensions, ajoutant le clavier, la souris, le joystick et la prise en charge des scripts, comme une semaine comme de nombreuses autres extensions. Alors que la plupart de ces DLL n'utilisaient pas DX directement, elles nécessitaient des connaissances et des liens avec DX simplement parce qu'elles tiraient des en-têtes qui exposaient DX. En ajoutant DX 11, cette complexité devait 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, mes interfaces exposées sont devenues plus propres lorsque des fonctions de membre privé ont été déplacées dans Pimpl, exposant uniquement les interfaces orientées vers l'avant.
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 utilisant ceci est Qt , où l'idée de base est de cacher l'implémentation ou le code spécifique à la plate-forme de 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é premium, tout va bien, mais s'il y a des bogues, 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.
Ainsi, comme dans presque toutes les décisions de conception, il y a des avantages et des inconvénients.
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.