web-dev-qa-db-fra.com

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

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

Mais les classes interface/pure-abstract en C++ ont également cette capacité, elles peuvent également être utilisées pour masquer les 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 d'usine dans l'en-tête de l'interface.

La comparaison est:

  1. Coût:

    Le coût de l'interface est plus faible, car vous n'avez même pas besoin de répéter l'implémentation de la fonction de wrapper public 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 moins bonnes que l'idiome PImpl, à la fois un accès mémoire supplémentaire. Je suppose que les performances sont les mêmes.

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 implémenté à 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?

49
ZijingWu

Premièrement, 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 des interfaces et remplace les méthodes virtuelles de la classe de base, etc. L'implémentation de PImpl est donc plus simple pas l'interface, c'est une classe simple 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 aussi longtemps que l'interface binaire reste la même. Désormais, 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 Problème d'interface binaire fragile

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

  3. Pour de nombreuses classes d'exceptions C++, la sécurité est une propriété importante. Souvent, vous devez composer plusieurs classes en une seule afin que si pendant l'opération sur plus d'un membre, aucun membre n'est modifié ou si vous avez une opération qui laissera le membre dans un état incohérent s'il lance et vous avez besoin que l'objet conteneur reste cohérent. Dans ce cas, vous implémentez l'opération en créant une nouvelle instance de PImpl et les échangez lorsque l'opération réussit.

En fait, l'interface peut également être utilisée uniquement pour la mise en œuvre, mais présente les inconvénients suivants:

  1. L'ajout d'une méthode non virtuelle ne rompt pas l'ABI, mais l'ajout d'une méthode virtuelle le fait. Les interfaces ne permettent donc pas du tout d'ajouter des méthodes, PImpl le permet.

  2. L'inteface ne peut être utilisée que via un pointeur/une référence, donc l'utilisateur doit veiller à la bonne gestion des ressources. D'un autre côté, les classes utilisant PImpl sont toujours des types de 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 n'aidera pas à la sécurité d'exception. Pour cela, vous avez besoin de l'indirection à l'intérieur de la classe.

51
Jan Hudec

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 intégrées par l'optimiseur du compilateur. Les fonctions PIMPL peuvent (et le seront probablement, car elles sont si courtes) être intégrées (peut-être plus d'une fois si la fonction IMPL est également petite). Un appel de fonction virtuelle ne peut pas être optimisé à moins que vous n'utilisiez des optimisations complètes d'analyse de programme qui prennent très longtemps.

Si votre classe PIMPL n'est pas utilisée de manière critique pour les performances, alors ce point n'a pas beaucoup d'importance, mais votre supposition que les performances sont les mêmes ne s'applique que dans certaines situations, pas toutes.

8
Chewy Gumball

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 recompilation des classes qui en dépendent, donc les temps sont plus rapides et le 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, donc les temps de compilation sont plus rapides.

L'idiome Pimpl est une optimisation au moment de la compilation, une technique de rupture de dépendance. Il réduit les gros fichiers d'en-tête. Il réduit votre 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 - alors gardez à l'esprit que les unités C++ prétraitées peuvent être très grandes. L'utilisation de Pimpl peut améliorer les temps de compilation.

Il existe d'autres meilleures techniques de suppression des dépendances - qui impliquent d'améliorer votre conception. Que représentent les méthodes privées? Quelle est la responsabilité unique? Devraient-ils être une autre classe?

5
Dave Hillier