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 !
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.
32 votes
En C++, pimpl doit être implémenté avec
const unique_ptr<XImpl>
plutôt queXImpl*
.1 votes
"Je n'ai jamais vu ce genre d'approche avant, ni dans les entreprises où j'ai travaillé, ni dans les projets open source". Qt ne l'utilise pratiquement jamais PAS.
0 votes
Bjarne Stoustrup recommande le nom _impl et non _pimpl.
0 votes
stackoverflow.com/questions/2346163/
1 votes
@NeilG Cela nécessiterait que la définition de XImpl soit visible par les clients, ce qui irait à l'encontre du modèle PImpl. Si X contient un membre unique_ptr<XImpl>, X n'est pas entièrement défini à moins que unique_ptr<XImpl> soit défini, ce qui n'est pas défini à moins que XImpl soit défini. Ainsi, tous les cpp clients autres que X.cpp ne compileront pas, à moins que nous perdions la moitié des avantages de Pimpl en incluant la définition de XImpl dans X.h.
0 votes
Oui, j'ai essayé. Le code dans votre commentaire le plus récent ne nécessite pas la définition de XImpl ou std::unique_ptr<XImpl>, donc il compile.
0 votes
Je l'ai essayé, pour revérifier. J'ai raison. Ça ne compile pas.
0 votes
Je n'ai pas de question. Quelle question devrais-je poser ?
0 votes
@KeithRussell Votre question est "Avez-vous besoin de définir
XImpl
avant de déclarerunique_ptr<XImpl>
en tant que membre d'une classeX
?" Vous semblez penser que la réponse est oui.0 votes
J'ai le sentiment que ce va-et-vient ne fait que diluer la section des commentaires sur cette question. Au lieu d'être simplement en désaccord avec moi, veuillez remplacer le pointeur dans l'exemple de l'OP par un unique_ptr, puis inclure X.h dans un second fichier Y.cpp (pas X.cpp, où se trouve la définition de XImpl). Si votre compilateur vous permet d'aller aussi loin, essayez d'instancier un X dans ce fichier Y.cpp.
0 votes
@KeithRussell Vous avez tort.
XImpl
ne doit être définie que dans le fichier d'implémentation ; elle peut être simplement déclarée comme une classe dans le fichier d'en-tête. Voir également cette question : stackoverflow.com/questions/9020372/1 votes
Nous avons tous deux tort. Cet exemple contient une petite mais importante différence : Contrairement au code de l'OP, le code de GotW a un destructeur explicite pour X, ce qui empêche le destructeur pour unique_ptr<XImpl> d'être défini en ligne dans un destructeur implicite pour X. C'est un excellent moyen de contourner le problème, qui se produit définitivement dans le code de l'OP si nous remplaçons le pointeur opaque par un unique_ptr. (Et je vais commencer à utiliser cette astuce - mais je la commenterai quand je le ferai, car elle repose sur des détails privés de la STL).
0 votes
Unique_ptr générera un appel au destructeur de XImpl, que nous ayons ou non (explicitement) défini un constructeur pour X. C'est l'intérêt d'utiliser unique_ptr. Le problème est que le destructeur de XImpl doit être déclaré dans toute unité de traduction contenant une définition (implicite ou non) du destructeur de X, sinon la génération de cet appel par unique_ptr échouera. Pour cette raison, afin de préserver le modèle PImpl, le destructeur de X doit être explicite (et défini en dehors de l'en-tête).
0 votes
Oui. Ce qui m'a échappé, c'est que la STL peut définir des classes de modèles "au coup par coup". Tant que nous cachons tous les appels à la plupart des constructeurs de unique_ptr et à son destructeur, tout va bien - et nous pouvons faire cela en supprimant la définition implicite du constructeur et du destructeur de X. Dans votre commentaire original, vous avez juste mentionné l'utilisation de unique_ptr, mais vous n'avez pas mentionné que le code de OP, tel qu'il est écrit, ne pourrait pas simplement avoir son pointeur opaque remplacé par un unique_ptr - OP devrait également définir le destructeur de X.
0 votes
@KeithRussell -ce que vous devez faire de toute façon lorsque vous utilisez l'idiome pimpl.
0 votes
Vrai. (En le regardant à nouveau, le code de l'OP manque à la fois d'un constructeur et d'un destructeur pour sa version opaque-pointeur de X, même s'il semble évident qu'il a l'intention que X possède pImpl dans un sens RAII).
0 votes
Addendum : L'opérateur d'affectation et le constructeur de copie implicites et générés doivent également être supprimés, tout comme le constructeur et le destructeur implicites par défaut.
2 votes
@NeilG maintenant je suppose que la manière recommandée est d'utiliser std::experimental::propagate_const<std::unique_ptr<impl>> pImpl
1 votes
@KeithRussell l'opérateur d'affectation et les constructeurs de copie générés seront supprimés simplement parce que unique_ptr n'a pas d'opérateur d'affectation / constructeur de copie disponible. Et l'opérateur d'affectation move et le constructeur move ne sont pas implicitement déclarés lorsqu'il y a un destructeur déclaré par l'utilisateur dans la classe. Je pense donc que l'exemple de la GotW est, sans surprise, correct. Mais en général, utiliser un unique_ptr sur un type incomplet semble être un territoire dangereux.
0 votes
@AndyBorrell N'ayez pas peur ! Je l'ai fait pendant des années depuis cette question, et j'adore ça. Vous ne pouvez pas le faire dans de nombreux cas, mais la chaîne d'outils vous dira quand ce sera le cas.