Question

Cette question a déjà une réponse ici :

Document d'information :

Le Idiome PIMPL (Pointeur vers IMPLementation) est une technique de masquage d'implémentation dans laquelle une classe publique encapsule une structure ou une classe qui ne peut pas être vue en dehors de la bibliothèque dont la classe publique fait partie.

Cela masque les détails et les données d'implémentation internes à l'utilisateur de la bibliothèque.

Lors de l'implémentation de cet idiome, pourquoi placeriez-vous les méthodes publiques sur la classe pimpl et non sur la classe publique puisque les implémentations de méthodes de classes publiques seraient compilées dans la bibliothèque et que l'utilisateur ne dispose que du fichier d'en-tête ?

Pour illustrer, ce code met le Purr() implémentation sur la classe impl et l'enveloppe également.

Pourquoi ne pas implémenter Purr directement sur la classe publique ?

// header file:
class Cat {
    private:
        class CatImpl;  // Not defined here
        CatImpl *cat_;  // Handle

    public:
        Cat();            // Constructor
        ~Cat();           // Destructor
        // Other operations...
        Purr();
};


// CPP file:
#include "cat.h"

class Cat::CatImpl {
    Purr();
...     // The actual implementation can be anything
};

Cat::Cat() {
    cat_ = new CatImpl;
}

Cat::~Cat() {
    delete cat_;
}

Cat::Purr(){ cat_->Purr(); }
CatImpl::Purr(){
   printf("purrrrrr");
}
Était-ce utile?

La solution

  • Parce que tu veux Purr() pouvoir utiliser des membres privés de CatImpl. Cat::Purr() ne serait pas autorisé à un tel accès sans friend déclaration.
  • Parce qu'on ne mélange alors pas les responsabilités :une classe met en œuvre, une classe avance.

Autres conseils

Je pense que la plupart des gens appellent cela l’idiome Handle Body.Voir le livre de James Coplien Advanced C++ Programming Styles and Idioms (Lien Amazon).Il est également connu sous le nom de chat de Cheshire à cause du personnage de Lewis Caroll qui s'efface jusqu'à ce qu'il ne reste plus que le sourire.

L'exemple de code doit être réparti sur deux ensembles de fichiers sources.Alors seul Cat.h est le fichier livré avec le produit.

CatImpl.h est inclus par Cat.cpp et CatImpl.cpp contient l'implémentation de CatImpl::Purr().Cela ne sera pas visible au public utilisant votre produit.

Fondamentalement, l’idée est de cacher autant que possible la mise en œuvre aux regards indiscrets.Ceci est particulièrement utile lorsque vous disposez d'un produit commercial livré sous la forme d'une série de bibliothèques accessibles via une API avec laquelle le code du client est compilé et lié.

Nous l'avons fait avec la réécriture du produit Orbix 3.3 de IONA en 2000.

Comme mentionné par d'autres, l'utilisation de sa technique découple complètement l'implémentation de l'interface de l'objet.Ensuite, vous n'aurez pas à recompiler tout ce qui utilise Cat si vous souhaitez simplement modifier l'implémentation de Purr().

Cette technique est utilisée dans une méthodologie appelée conception par contrat.

Pour ce qui vaut la peine, cela sépare l’implémentation de l’interface.Ceci n’est généralement pas très important dans les projets de petite taille.Mais, dans les grands projets et bibliothèques, il peut être utilisé pour réduire considérablement les temps de construction.

Considérons que la mise en œuvre de Cat peut inclure de nombreux en-têtes, peut impliquer une méta-programmation de modèles qui prend du temps à se compiler seule.Pourquoi un utilisateur qui souhaite simplement utiliser le Cat faut-il inclure tout ça ?Par conséquent, tous les fichiers nécessaires sont masqués à l’aide de l’idiome pimpl (d’où la déclaration forward de CatImpl), et l'utilisation de l'interface n'oblige pas l'utilisateur à les inclure.

Je développe une bibliothèque pour l'optimisation non linéaire (lire "beaucoup de mathématiques désagréables"), qui est implémentée dans des modèles, donc la plupart du code est dans les en-têtes.La compilation prend environ cinq minutes (sur un processeur multicœur décent) et il suffit d'analyser les en-têtes dans un fichier autrement vide. .cpp prend environ une minute.Ainsi, toute personne utilisant la bibliothèque doit attendre quelques minutes à chaque fois qu'elle compile son code, ce qui rend le développement assez compliqué. fastidieux.Cependant, en masquant l'implémentation et les en-têtes, on inclut simplement un simple fichier d'interface, qui se compile instantanément.

Cela n'a pas nécessairement quelque chose à voir avec la protection de l'implémentation contre la copie par d'autres sociétés - ce qui n'arriverait probablement pas de toute façon, à moins que le fonctionnement interne de votre algorithme puisse être deviné à partir des définitions des variables membres (si c'est le cas, c'est probablement pas très compliqué et ne vaut pas la peine d'être protégé en premier lieu).

Si votre classe utilise l'idiome pimpl, vous pouvez éviter de modifier le fichier d'en-tête sur la classe publique.

Cela vous permet d'ajouter/supprimer des méthodes à la classe pimpl, sans modifier le fichier d'en-tête de la classe externe.Vous pouvez également ajouter/supprimer des #includes au bouton.

Lorsque vous modifiez le fichier d'en-tête de la classe externe, vous devez recompiler tout ce qui #l'inclut (et si l'un d'entre eux est un fichier d'en-tête, vous devez recompiler tout ce qui #l'inclut, et ainsi de suite)

En règle générale, la seule référence à la classe Pimpl dans l'en-tête de la classe Owner (Cat dans ce cas) serait une déclaration directe, comme vous l'avez fait ici, car cela peut réduire considérablement les dépendances.

Par exemple, si votre classe Pimpl a ComplicatedClass comme membre (et pas seulement un pointeur ou une référence à celui-ci), vous devrez alors avoir ComplicatedClass entièrement défini avant son utilisation.En pratique, cela signifie inclure "ComplicatedClass.h" (qui inclura également indirectement tout ce dont dépend ComplicatedClass).Cela peut conduire à un seul remplissage d'en-tête qui contient beaucoup de choses, ce qui est mauvais pour la gestion de vos dépendances (et de vos temps de compilation).

Lorsque vous utilisez l'idion pimpl, il vous suffit de #inclure les éléments utilisés dans l'interface publique de votre type de propriétaire (qui serait Cat ici).Ce qui améliore les choses pour les personnes utilisant votre bibliothèque et signifie que vous n'avez pas à vous soucier des personnes qui dépendent d'une partie interne de votre bibliothèque - soit par erreur, soit parce qu'elles veulent faire quelque chose que vous n'autorisez pas, alors elles #définissent private public avant d’inclure vos fichiers.

S'il s'agit d'une classe simple, il n'y a généralement aucune raison d'utiliser un Pimpl, mais lorsque les types sont assez volumineux, cela peut être d'une grande aide (surtout pour éviter de longs temps de construction)

Eh bien, je ne l'utiliserais pas.J'ai une meilleure alternative :

foo.h :

class Foo {
public:
    virtual ~Foo() { }
    virtual void someMethod() = 0;

    // This "replaces" the constructor
    static Foo *create();
}

foo.cpp :

namespace {
    class FooImpl: virtual public Foo {

    public:
        void someMethod() { 
            //....
        }     
    };
}

Foo *Foo::create() {
    return new FooImpl;
}

Ce modèle a-t-il un nom ?

En tant que programmeur Python et Java, j'aime cela beaucoup plus que l'idiome pImpl.

Nous utilisons l'idiome PIMPL afin d'émuler une programmation orientée aspect où les aspects pré, post et erreur sont appelés avant et après l'exécution d'une fonction membre.

struct Omg{
   void purr(){ cout<< "purr\n"; }
};

struct Lol{
  Omg* omg;
  /*...*/
  void purr(){ try{ pre(); omg-> purr(); post(); }catch(...){ error(); } }
};

Nous utilisons également un pointeur vers la classe de base pour partager différents aspects entre plusieurs classes.

L'inconvénient de cette approche est que l'utilisateur de la bibliothèque doit prendre en compte tous les aspects qui vont être exécutés, mais ne voit que sa classe.Cela nécessite de parcourir la documentation pour détecter tout effet secondaire.

Placer l'appel à impl-> Purr dans le fichier cpp signifie qu'à l'avenir, vous pourrez faire quelque chose de complètement différent sans avoir à modifier le fichier d'en-tête.Peut-être que l'année prochaine, ils découvriront une méthode d'assistance qu'ils auraient pu appeler à la place et pourront ainsi modifier le code pour l'appeler directement et ne pas utiliser du tout impl->Purr.(Oui, ils pourraient obtenir la même chose en mettant à jour la méthode impl::Purr réelle, mais dans ce cas, vous êtes coincé avec un appel de fonction supplémentaire qui ne fait rien d'autre que d'appeler la fonction suivante à son tour)

Cela signifie également que l'en-tête n'a que des définitions et n'a aucune implémentation permettant une séparation plus nette, ce qui est tout l'intérêt de l'idiome.

Je viens de mettre en œuvre mon premier cours de boutons au cours des derniers jours.Je l'ai utilisé pour éliminer les problèmes que je rencontrais, notamment winsock2.h dans Borland Builder.Cela semblait gâcher l'alignement des structures et comme j'avais des éléments de socket dans les données privées de la classe, ces problèmes se propageaient à n'importe quel fichier cpp incluant l'en-tête.

En utilisant pimpl, winsock2.h a été inclus dans un seul fichier cpp où je pouvais mettre un terme au problème et ne pas craindre qu'il revienne me mordre.

Pour répondre à la question initiale, l'avantage que j'ai trouvé en transmettant les appels à la classe pimpl était que la classe pimpl est la même que ce qu'aurait été votre classe d'origine avant que vous ne la pimpliez, et en plus vos implémentations ne sont pas réparties sur 2. cours d'une manière étrange.Il est beaucoup plus clair d'implémenter les publics simplement de les transmettre à la classe Pimpl.

Comme le disait M. Nodet, une classe, une responsabilité.

Je ne sais pas si c'est une différence qui mérite d'être mentionnée mais...

Serait-il possible d'avoir l'implémentation dans son propre espace de noms et d'avoir un espace de noms public wrapper/bibliothèque pour le code que l'utilisateur voit :

catlib::Cat::Purr(){ cat_->Purr(); }
cat::Cat::Purr(){
   printf("purrrrrr");
}

De cette façon, tout le code de la bibliothèque peut utiliser l'espace de noms cat et, lorsque le besoin d'exposer une classe à l'utilisateur se fait sentir, un wrapper pourrait être créé dans l'espace de noms catlib.

Je trouve révélateur que, malgré la notoriété du terme bouton, je ne le vois pas apparaître très souvent dans la vie réelle (par ex.dans les projets open source).

Je me demande souvent si les « avantages » sont exagérés ;oui, vous pouvez masquer encore plus certains détails de votre implémentation, et oui, vous pouvez modifier votre implémentation sans changer l'en-tête, mais il n'est pas évident que ce soient de gros avantages en réalité.

Autrement dit, il n'est pas clair que votre implémentation soit nécessaire. que bien caché, et il est peut-être assez rare que les gens modifient réellement uniquement la mise en œuvre ;dès que vous devez ajouter de nouvelles méthodes, par exemple, vous devez quand même modifier l'en-tête.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top