Quel est l'intérêt du modèle PImpl alors que nous pouvons utiliser l'interface dans le même but en C ++?

49

Je vois beaucoup de code source qui utilise l'idiome PImpl en C ++. Je suppose que son objectif est de masquer les données / types / implémentations privées, de manière à pouvoir supprimer la dépendance, puis à réduire le temps de compilation et le problème d'inclusion d'en-tête.

Mais les classes d’interface / pure-abstract en C ++ ont également cette capacité, elles peuvent également être utilisées pour masquer des données / type / implémentation. Et pour que l'appelant ne voie que l'interface lors de la création d'un objet, nous pouvons déclarer une méthode factory dans l'en-tête de l'interface.

La comparaison est:

  1. Coût :

    Le coût de l'interface est moins élevé, car vous n'avez même pas besoin de répéter l'implémentation de la fonction wrapper publique void Bar::doWork() { return m_impl->doWork(); }, il vous suffit de définir la signature dans l'interface.

  2. Bien compris :

    La technologie d'interface est mieux comprise par tous les développeurs C ++.

  3. Performance :

    Les performances de l'interface ne sont pas pires que l'idiome PImpl, un accès supplémentaire à la mémoire. Je suppose que la performance est la même.

Voici le pseudocode pour illustrer ma question:

// Forward declaration can help you avoid include BarImpl header, and those included in BarImpl header.
class BarImpl;
class Bar
{
public:
    // public functions
    void doWork();
private:
    // You don't need to compile Bar.cpp after changing the implementation in BarImpl.cpp
    BarImpl* m_impl;
};

Le même objectif peut être mis en œuvre à l'aide de l'interface:

// Bar.h
class IBar
{
public:
    virtual ~IBar(){}
    // public functions
    virtual void doWork() = 0;
};

// to only expose the interface instead of class name to caller
IBar* createObject();

Alors, quel est l'intérêt de PImpl?

ZijingWu
la source

Réponses:

50

Tout d'abord, PImpl est généralement utilisé pour les classes non polymorphes. Et lorsqu'une classe polymorphe a PImpl, elle reste généralement polymorphe, c'est-à-dire qu'elle implémente toujours les interfaces et remplace les méthodes virtuelles de la classe de base, etc. Donc, une implémentation plus simple de PImpl n’est pas une interface, c’est une simple classe contenant directement les membres!

Il y a trois raisons d'utiliser PImpl:

  1. Rendre l' interface binaire (ABI) indépendante des membres privés. Il est possible de mettre à jour une bibliothèque partagée sans recompiler le code dépendant, mais uniquement tant que l' interface binaire reste la même. Maintenant, presque tout changement d'en-tête, à l'exception de l'ajout d'une fonction non membre et de l'ajout d'une fonction membre non virtuelle, modifie l'ABI. L'idiome PImpl déplace la définition des membres privés dans la source et dissocie ainsi l'ABI de leur définition. Voir le problème de l'interface binaire fragile

  2. Lorsqu'un en-tête change, toutes les sources, y compris celui-ci, doivent être recompilées. Et la compilation en C ++ est plutôt lente. Ainsi, en déplaçant les définitions des membres privés vers la source, l'idiome PImpl réduit le temps de compilation, car il faut extraire moins de dépendances dans l'en-tête, et réduit d'autant le temps de compilation après les modifications, car les dépendants n'ont pas besoin d'être recompilés. (ok, cela s'applique aussi à l'interface + fonction d'usine avec la classe concrète cachée).

  3. Pour de nombreuses classes en C ++, la sécurité est une propriété importante. Il est souvent nécessaire de composer plusieurs classes en une seule, de sorte que, si lors de l'opération sur plusieurs lancements de membres, aucun des membres ne soit modifié ou que vous ayez une opération qui laissera le membre dans un état incohérent s'il est lancé et que vous souhaitez que l'objet contenant soit conservé. cohérent. Dans ce cas, vous implémentez l'opération en créant une nouvelle instance de PImpl et en les échangeant lorsque l'opération aboutit.

En réalité, interface peut également être utilisé pour masquer les implémentations uniquement, mais présente les inconvénients suivants:

  1. Ajouter une méthode non virtuelle ne casse pas ABI, mais en ajouter une virtuelle. Les interfaces ne permettent donc pas l'ajout de méthodes, contrairement à PImpl.

  2. Inteface ne peut être utilisé que via un pointeur / une référence, l'utilisateur doit donc s'occuper de la gestion appropriée des ressources. D'autre part, les classes utilisant PImpl sont toujours des types valeur et gèrent les ressources en interne.

  3. L'implémentation cachée ne peut pas être héritée, la classe avec PImpl le peut.

Et bien sûr, l'interface ne va pas aider avec la sécurité des exceptions. Vous avez besoin de l'indirection dans la classe pour cela.

Jan Hudec
la source
Bon point. Ajouter une fonction publique en Implclasse ne cassera pas le ABI, mais ajouter une fonction publique dans une interface cassera ABI.
ZijingWu
1
L'ajout d'une fonction / méthode publique ne doit pas nécessairement casser l'ABI; les changements strictement additifs ne sont pas des changements brisés. Cependant, vous devez être très prudent; la médiation de code entre le front-end et le back-end doit faire face à toutes sortes d'amusement avec le versioning. Tout devient difficile. (C'est ce genre de choses qui explique pourquoi de nombreux projets logiciels préfèrent le C au C ++; cela leur permet de mieux comprendre ce qu'est l'ABI.)
Donal Fellows
3
@DonalFellows: L'ajout d'une fonction / méthode publique ne rompt pas ABI. L'ajout d'une méthode virtuelle le fait et le fait toujours, sauf si l'utilisateur est empêché d'hériter de l'objet (ce que réalise PImpl).
Jan Hudec
4
Afin de clarifier pourquoi l' ajout d' une rupture de méthode virtuelle de l'ABI. Cela a à voir avec le lien. La liaison à des méthodes non virtuelles se fait par nom, que la bibliothèque soit statique ou dynamique. Ajouter une autre méthode non virtuelle, c'est simplement ajouter un autre nom auquel créer un lien. Cependant, les méthodes virtuelles sont des index dans une table et les codes externes sont liés efficacement par index. L'ajout d'une méthode virtuelle peut déplacer d'autres méthodes dans vtable sans que le code externe puisse le savoir.
SnakE
1
PImpl réduit également le temps de compilation initial. Par exemple, si mon implémentation a un champ de type type (par exemple unordered_set), sans PImpl, je dois entrer #include <unordered_set>dans MyClass.h. Avec Pimpl, je ne dois inclure dans le .cppfichier, pas le .hfichier, et donc tout le reste qui le comprend ...
Martin C. Martin
7

Je veux juste aborder votre point de performance. Si vous utilisez une interface, vous avez nécessairement créé des fonctions virtuelles qui NE SERONT PAS alignées par l'optimiseur du compilateur. Les fonctions PIMPL peuvent (et le seront probablement, car elles sont si courtes) en ligne (peut-être plus d'une fois si la fonction IMPL est également petite). Un appel de fonction virtuel ne peut pas être optimisé sauf si vous utilisez des optimisations d'analyse de programme complètes, qui prennent beaucoup de temps.

Si votre classe PIMPL n'est pas utilisée de manière critique en termes de performances, alors ce point importe peu, mais votre hypothèse selon laquelle les performances sont identiques ne s'applique que dans certaines situations, pas toutes.

Chewy Gumball
la source
1
Avec l'utilisation de l'optimisation du temps de liaison, la majeure partie de la surcharge liée à l'utilisation de l'idiome de pimpl est supprimée. Les fonctions d'encapsuleur ainsi que les fonctions d'implémentation peuvent être en ligne, ce qui rend les appels de fonction efficaces comme une classe normale implémentée dans un en-tête.
Goji
Cela n’est vrai que si vous utilisez l’optimisation temps-lien. Le point important de PImpl est que le compilateur n'a pas l'implémentation, pas même la fonction de transfert. L'éditeur de liens fait cependant.
Martin C. Martin
5

Cette page répond à votre question. Beaucoup de gens sont d'accord avec votre affirmation. L'utilisation de Pimpl présente les avantages suivants par rapport aux champs / membres privés.

  1. La modification des variables de membre privé d'une classe ne nécessite pas de recompiler les classes qui en dépendent. Les temps sont donc plus rapides et le problème FragileBinaryInterfaceProblem est réduit.
  2. Le fichier d'en-tête n'a pas besoin d'inclure # les classes qui sont utilisées 'par valeur' ​​dans les variables membres privées, ainsi les temps de compilation sont plus rapides.

Le langage Pimpl est une optimisation à la compilation, une technique de rupture de dépendance. Il réduit les fichiers d'en-tête volumineux. Cela réduit l'en-tête à l'interface publique uniquement. La longueur de l'en-tête est importante en C ++ lorsque vous réfléchissez à son fonctionnement. #include concatène efficacement le fichier d’en-tête au fichier source. Gardez donc à l’esprit que les unités C ++ pré-traitées peuvent être très volumineuses. Utiliser Pimpl peut améliorer les temps de compilation.

Il existe d'autres techniques de rupture de dépendance améliorées, qui consistent à améliorer votre conception. Que représentent les méthodes privées? Quelle est la responsabilité unique? Devraient-ils être une autre classe?

Dave Hillier
la source
PImpl n'est pas du tout une optimisation à l'exécution. Les allocations ne sont pas triviales et pimpl signifie une allocation supplémentaire. C'est une technique de rupture de dépendance et d'optimisation du temps de compilation uniquement.
Jan Hudec
@JanHudec clarifié.
Dave Hillier
@DaveHillier, +1 pour la mention FragileBinaryInterfaceProblem
ZijingWu