Est-ce que l’idiome pImpl est vraiment utilisé dans la pratique?

Je lis le livre “Exceptional C ++” de Herb Sutter, et dans ce livre j’ai appris le langage de pImpl. Fondamentalement, l’idée est de créer une structure pour les objects private d’une class et de les allouer dynamicment pour réduire le temps de compilation (et aussi pour mieux cacher les implémentations privées).

Par exemple:

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

pourrait être changé pour:

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

et, dans le CPP, la définition:

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

Cela semble assez intéressant, mais je n’ai jamais vu ce type d’approche auparavant, ni dans les entresockets que j’ai travaillées, ni dans les projets open source que j’ai vus du code source. Alors, je me demande si cette technique est vraiment utilisée dans la pratique?

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

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

    Bien sûr, il est utilisé, et dans mon projet, dans presque toutes les classes, pour plusieurs raisons que vous avez mentionnées:

    • dissimulation de données
    • le temps de recompilation est vraiment réduit, puisque seul le fichier source doit être reconstruit, mais pas l’en-tête, et chaque fichier qui l’inclut
    • compatibilité binary. Comme la déclaration de classe ne change pas, vous pouvez simplement mettre à jour la bibliothèque (en supposant que vous créez une bibliothèque)

    cette technique est-elle recommandée pour être utilisée dans des 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 la suivante: mesurez et évaluez ce que vous gagnez et perdez.

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

    Mais pour tout, vous ne devriez jamais utiliser n’importe quoi sans précaution. Pensez toujours avant de l’utiliser. Évaluez les avantages que cela vous procure et leur valeur.

    Les avantages que cela peut vous apporter sont:

    • aide à garder la compatibilité binary des bibliothèques partagées
    • cacher certains détails internes
    • cycles de recompilation décroissants

    Ceux-ci peuvent ou non être 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 comstacknt toujours une fois depuis le début.

    Les inconvénients possibles sont (également ici, selon la mise en œuvre 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
    • effort de maintenance accru (vous devez écrire au moins les fonctions de transfert)
    • Perte de performance (le compilateur peut ne pas être en mesure d’intégrer des éléments comme il le fait avec une implémentation naïve de votre classe)

    Alors, donnez soigneusement une valeur à tout et évaluez-le 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 wrapper C ++ pour l’appel de stat linux. Ici, la structure de l’en-tête C peut être différente, en fonction de ce #defines est défini. Et comme l’en-tête de mon wrapper ne peut pas tous les contrôler, je #include seulement #include dans mon fichier .cxx et évite ces problèmes.

    D’accord avec tous les autres sur les produits, mais laissez-moi 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 raison principale pour laquelle vous ne voyez pas les méthodes de template définies dans les fichiers CPP)

    Vous pouvez toujours vous référer aux sous-classes modélisées, mais puisque vous devez les inclure toutes, tous les avantages du «découplage de l’implémentation» sur la compilation (en évitant d’inclure tout code spécifique de platoform, en raccourcissant la compilation) sont perdus.

    Est un bon paradigme pour la POO classique (basée sur l’inheritance) mais pas pour la programmation générique (basée sur la spécialisation).

    D’autres personnes ont déjà fourni les avantages 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 thread utilisée par OpenSceneGraph . L’idée principale est de supprimer de l’en-tête (par exemple ) tout le code spécifique à la plate-forme, car les variables d’état internes (par exemple, les descripteurs de thread) diffèrent d’une plateforme à l’autre. De cette façon, vous pouvez comstackr du code sur votre bibliothèque sans connaître les particularités des autres plates-formes, car tout est caché.

    Je considérerais principalement PIMPL pour les classes exposées à être utilisées comme API par d’autres modules. Cela présente de nombreux avantages, car la recompilation des modifications apscopes dans l’implémentation PIMPL n’affecte pas le rest du projet. De plus, pour les classes API, elles favorisent une compatibilité binary (les modifications apscopes à une implémentation de module n’affectent pas les clients de ces modules, elles ne doivent pas être recompilées car la nouvelle implémentation a la même interface binary).

    En ce qui concerne l’utilisation de PIMPL pour chaque classe, j’envisagerais 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.

    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 incorporé (SetTopBox).

    Le but particulier de cet idoim dans notre projet était de cacher les types de classes utilisés par XImpl. Plus précisément, nous l’avons utilisé pour masquer les détails d’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 pour l’autre. La disposition de la classe X est restée la même quel que soit le plateau.

    J’avais l’habitude d’utiliser cette technique dans le passé, mais je me suis ensuite éloignée.

    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 amenant les utilisateurs de la classe à utiliser une interface abstraite et à faire en sorte que les détails de l’implémentation soient la classe concrète.

    Les avantages de pImpl sont les suivants:

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

    2. Si vous avez une suite de classes (un module) telle que plusieurs classes accèdent au même “impl”, les utilisateurs du module utiliseront uniquement 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 ne puissiez avoir qu’une seule implémentation “production”, en utilisant une interface abstraite, vous pouvez également créer une implémentation “simulée” 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 manière de stocker pImpl. Un pointeur brut et vous aviez des problèmes à propos de votre classe non-copiable. Un ancien auto_ptr ne fonctionnerait pas avec la classe déclarée (pas sur tous les compilateurs de toute façon). Donc, les gens ont 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, ce à quoi vous ne vous attendez pas (modifiez-en une et les deux sont modifiées). La solution consistait donc souvent à utiliser le pointeur brut pour le point interne et à rendre la classe non-copiable et à renvoyer un shared_ptr à la place. Donc deux appels à nouveau. (En fait, 3 anciens shared_ptr vous ont donné un deuxième).

    3. Techniquement, ce n’est pas vraiment correct car la constance n’est pas transmise à un pointeur membre.

    En général, je suis donc passé de pImpl à une utilisation abstraite des interfaces (et des méthodes d’usine pour créer des instances).

    Comme beaucoup d’autres l’ont dit, l’idiome de Pimpl permet d’obtenir une indépendance totale en matière de dissimulation et de compilation des informations, malheureusement avec le coût de la perte de performances (indirection du pointeur supplémentaire) et le besoin de mémoire supplémentaire (le pointeur membre lui-même). Le coût supplémentaire peut être critique dans le développement de logiciels intégré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 lacune de C ++ où, sans récurrence aux interfaces de type C (méthodes globales avec un pointeur opaque comme paramètre), il est impossible d’avoir une véritable indépendance de dissimulation et de compilation des informations sans inconvénient de ressources: la déclaration d’une classe, qui doit être incluse par ses utilisateurs, exporte non seulement l’interface de la classe (méthodes publiques) requirejse par les utilisateurs, mais également ses composants internes (membres privés), non nécessaires aux utilisateurs.

    Il est utilisé dans la pratique dans de nombreux projets. Son utilité dépend fortement du type de projet. L’un des projets les plus en vue est Qt , où l’idée de base est de cacher 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 implemetations privées est de qualité supérieure, tout va bien, mais s’il y a des bogues, l’utilisateur / développeur a un problème, car il ne s’agit que d’un pointeur muet vers une implémentation cachée, même s’il possède 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.

    Un avantage que je peux voir est que cela 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.

    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 contenait déjà la plupart des fonctionnalités DX, de sorte qu’aucune des interfaces DX n’était utilisée directement; ils étaient juste définis dans les en-têtes en tant que membres privés. Le moteur utilise des DLL en tant qu’extensions, en ajoutant le clavier, la souris, le joystick et la prise en charge des scripts, ainsi que de nombreuses autres extensions. Alors que la plupart de ces DLL n’utilisaient pas directement DX, elles nécessitaient des connaissances et des liens avec DX simplement parce qu’elles contenaient 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 en déplaçant les fonctions de membre privées dans le Pimpl, exposant uniquement les interfaces frontales.