Le modèle de conception PImpl (Pointer to Implementation), également connu sous le nom de Opaque Pointer, est apparu comme un moyen de séparer l'interface et l'implémentation d'une classe en C++. Cela est particulièrement important pour garantir la compatibilité des interfaces binaires (ABI), accélérer la compilation et cacher les détails de l'implémentation à l'utilisateur de la classe.
Historique de la question.
Dans un grand nombre de projets C++, il est souvent nécessaire de modifier les implémentations des classes sans changer l'interface publique et sans recompiler les clients de ces classes. Le problème réside dans le fait que toute modification dans les fichiers d'en-tête nécessite de reconstruire tous les modules dépendants, ce qui peut être très coûteux dans de grandes bases de code. PImpl permet de minimiser la recompilation et assure une meilleure encapsulation.
Problème.
La méthode standard de définition d'une classe avec des membres privés dans un fichier d'en-tête nécessite de connaître tous ces membres au moment de la compilation. Lors de l'extension ou de la modification de ceux-ci, tous les fichiers qui incluent cet en-tête doivent être recompilés. De plus, cela expose les détails de l'implémentation/structure du client, pouvant nuire à la sécurité et à l'intégrité architecturale.
Solution.
PImpl réalise la dissimulation de l'implémentation grâce à l'utilisation d'un pointeur vers une structure d'implémentation déclarée en avance (Impl struct/class), définie dans le .cpp. Cela permet de modifier l'implémentation sans toucher à l'interface.
Exemple de code :
// Widget.h class Widget { public: Widget(); ~Widget(); void doSomething(); private: struct Impl; Impl* pimpl; // pointeur opaque }; // Widget.cpp #include "Widget.h" struct Widget::Impl { int secret; }; Widget::Widget() : pimpl(new Impl{42}) {} // secret à l'intérieur Widget::~Widget() { delete pimpl; } void Widget::doSomething() { pimpl->secret += 1; }
Caractéristiques clés :
Peut-on utiliser std::unique_ptr au lieu d'un pointeur brut dans PImpl ?
Oui, la méthode moderne et sûre est d'utiliser std::unique_ptr (ou std::shared_ptr s'il est nécessaire de partager la propriété). Cela permet de gérer la mémoire correctement et d'éviter d'écrire explicitement un destructeur/un opérateur de copie pour un pointeur brut :
private: std::unique_ptr<Impl> pimpl;
Peut-on rendre une classe avec PImpl déplaçable mais non copiable ?
Oui, si vous fournissez un constructeur/un opérateur de déplacement, mais supprimez la copie. Par exemple :
Widget(Widget&&) noexcept = default; Widget& operator=(Widget&&) noexcept = default; Widget(const Widget&) = delete; Widget& operator=(const Widget&) = delete;
Y a-t-il un surcoût en performance lors de l'utilisation de PImpl ?
Oui, à cause de la déréférencement du pointeur et de l'allocation de mémoire dynamique supplémentaire (allocation sur le tas). Pour les structures critiques en performance, cela peut être un inconvénient significatif.
Une grande entreprise a mis en œuvre PImpl pour toutes les classes répétées, y compris pour des structures de données simples, ce qui a conduit à un ralentissement significatif des opérations simples en raison de la déréférencement constant des pointeurs.
Avantages :
Inconvénients :
Dans un projet avec une bibliothèque d'interface utilisateur à long terme, PImpl a été appliqué uniquement aux widgets complexes avec un contenu interne souvent changeant, tout en maintenant un ABI stable pour les clients tiers.
Avantages :
Inconvénients :