Je me demandais ce qui inciterait un programmeur à choisir entre l'idiome Pimpl ou la classe virtuelle pure et l'héritage.
Je comprends que le langage pimpl est fourni avec une indirection supplémentaire supplémentaire explicite pour chaque méthode publique et le temps système nécessaire à la création d’objet.
La classe virtuelle pure, d’autre part, est assortie d’un indirection implicite (vtable) pour l’implémentation héritée et je comprends qu’il n’ya pas de surcharge de création d’objet.
EDIT: Mais vous auriez besoin d'une fabrique si vous créez l'objet de l'extérieur
Qu'est-ce qui rend la classe virtuelle pure moins souhaitable que le langage pimpl?
Lors de l'écriture d'une classe C++, il convient de se demander si ça va être
Un type de valeur
Copier par valeur, l'identité n'a jamais d'importance. Il convient que ce soit une clé dans un std :: map. Exemple, une classe "chaîne", ou une classe "date", ou une classe "nombre complexe". "Copier" les instances d'une telle classe a du sens.
Un type d'entité
L'identité est importante. Toujours passé par référence, jamais par "valeur". Souvent, cela n’a aucun sens de "copier" des instances de la classe. Lorsque cela a du sens, une méthode "Clone" polymorphe est généralement plus appropriée. Exemples: une classe Socket, une classe Database, une classe "policy", tout ce qui constituerait une "fermeture" dans un langage fonctionnel.
PImpl et la classe de base abstraite pure sont des techniques permettant de réduire les dépendances à la compilation.
Cependant, je n'utilise jamais pImpl que pour implémenter les types Value (type 1), et parfois seulement lorsque je souhaite réellement minimiser les dépendances de couplage et de compilation. Souvent, cela ne vaut pas la peine. Comme vous l'avez fait remarquer à juste titre, la charge syntaxique est plus lourde car vous devez écrire des méthodes de transfert pour toutes les méthodes publiques. Pour les classes de type 2, j'utilise toujours une classe de base abstraite pure avec les méthodes de fabrique associées.
Pointer to implementation
vise généralement à masquer les détails de la mise en œuvre structurelle. Interfaces
concerne l'instanciation de différentes implémentations. Ils servent vraiment deux objectifs différents.
L'idiome pimpl vous aide à réduire les dépendances et les délais de construction, en particulier dans les applications volumineuses, et minimise l'exposition des en-têtes des détails d'implémentation de votre classe à une unité de compilation. Les utilisateurs de votre classe ne devraient même pas avoir besoin de connaître l'existence d'un bouton (sauf en tant que pointeur cryptique auquel ils ne sont pas au courant!).
Les classes abstraites (virtuels purs) sont une chose dont vos clients doivent être conscients: si vous essayez de les utiliser pour réduire le couplage et les références circulaires, vous devez leur permettre de créer vos objets (par exemple, via des méthodes ou des classes de fabrique, injection de dépendance ou autres mécanismes).
Je cherchais une réponse à la même question. Après avoir lu quelques articles et un peu de pratique je préfère utiliser "interfaces de classe virtuelle pure".
Le seul inconvénient ( sur lequel j'essaie d'enquêter ) est que l'idiome de pimpl pourrait être plus rapide
Il existe un problème très réel avec les bibliothèques partagées: le langage pimpl contourne nettement ce que les virtuals purs ne peuvent pas faire: vous ne pouvez pas modifier/supprimer en toute sécurité les membres d'une classe sans obliger les utilisateurs de la classe à recompiler leur code. Cela peut être acceptable dans certaines circonstances, mais pas par exemple. pour les bibliothèques système.
Pour expliquer le problème en détail, considérez le code suivant dans votre bibliothèque/en-tête partagé:
// header
struct A
{
public:
A();
// more public interface, some of which uses the int below
private:
int a;
};
// library
A::A()
: a(0)
{}
Le compilateur émet un code dans la bibliothèque partagée qui calcule l'adresse du nombre entier à initialiser comme étant un certain décalage (probablement zéro dans ce cas, car il s'agit du seul membre) du pointeur sur l'objet A qu'il sait être this
.
Du côté utilisateur du code, un new A
allouera d'abord sizeof(A)
octets de mémoire, puis remettra un pointeur sur cette mémoire au constructeur A::A()
sous la forme this
.
Si, dans une révision ultérieure de votre bibliothèque, vous décidez de supprimer l'entier, de le rendre plus grand, d'ajouter un membre ou d'ajouter un membre, il y aura un décalage entre la quantité de code allouée par la mémoire de l'utilisateur et les décalages attendus par le code constructeur. Si vous êtes chanceux, le résultat est probable: si vous en avez moins, votre logiciel se comporte étrangement.
En pimplant, vous pouvez ajouter et supprimer en toute sécurité des membres de données à la classe interne, car l'allocation de mémoire et l'appel de constructeur ont lieu dans la bibliothèque partagée:
// header
struct A
{
public:
A();
// more public interface, all of which delegates to the impl
private:
void * impl;
};
// library
A::A()
: impl(new A_impl())
{}
Il ne vous reste plus qu'à maintenir votre interface publique libre de tout membre de données autre que le pointeur sur l'objet d'implémentation, et vous êtes à l'abri de cette classe d'erreurs.
Edit: Je devrais peut-être ajouter que la seule raison pour laquelle je parle du constructeur est que je ne voulais pas fournir plus de code - la même argumentation s'applique à toutes les fonctions qui accèdent aux membres des données.
Je déteste les boutons! Ils font la classe moche et pas lisible. Toutes les méthodes sont redirigées vers un bouton. Vous ne voyez jamais dans les en-têtes quelles fonctionnalités ont la classe, vous ne pouvez donc pas la refactoriser (par exemple, changez simplement la visibilité d'une méthode). La classe se sent "enceinte". Je pense que l'utilisation d'iterfaces est préférable et qu'elle est suffisante pour masquer l'implémentation du client. Vous pouvez même laisser une classe implémenter plusieurs interfaces pour les contenir. Il faut préférer les interfaces! Remarque: vous n’avez pas nécessairement besoin de la classe usine. Ce qui est pertinent, c'est que les clients de la classe communiquent avec leurs instances via l'interface appropriée ... Le masquage de méthodes privées me semble une étrange paranoïa et je ne vois pas pourquoi ce serait parce que nous avons des interfaces.
Nous ne devons pas oublier que l'héritage est un couplage plus fort et plus étroit que la délégation. Je prendrais également en compte toutes les questions soulevées dans les réponses fournies pour décider quels idiomes de conception utiliser pour résoudre un problème particulier.
Bien que largement couvert dans les autres réponses, je peux peut-être être un peu plus explicite sur l'un des avantages de pimpl par rapport aux classes de base virtuelles:
Une approche pimpl est transparente du point de vue de l'utilisateur, ce qui signifie que vous pouvez par exemple créer des objets de la classe sur la pile et les utiliser directement dans des conteneurs. Si vous essayez de masquer l'implémentation à l'aide d'une classe de base virtuelle abstraite, vous devez renvoyer un pointeur partagé à la classe de base depuis une fabrique, ce qui complique son utilisation. Considérez le code client équivalent suivant:
// Pimpl
Object pi_obj(10);
std::cout << pi_obj.SomeFun1();
std::vector<Object> objs;
objs.emplace_back(3);
objs.emplace_back(4);
objs.emplace_back(5);
for (auto& o : objs)
std::cout << o.SomeFun1();
// Abstract Base Class
auto abc_obj = ObjectABC::CreateObject(20);
std::cout << abc_obj->SomeFun1();
std::vector<std::shared_ptr<ObjectABC>> objs2;
objs2.Push_back(ObjectABC::CreateObject(13));
objs2.Push_back(ObjectABC::CreateObject(14));
objs2.Push_back(ObjectABC::CreateObject(15));
for (auto& o : objs2)
std::cout << o->SomeFun1();
Si je comprends bien, ces deux choses ont des finalités complètement différentes. Le but de l'idiot de bouton est essentiellement de vous donner un aperçu de votre implémentation afin que vous puissiez faire des choses comme des échanges rapides pour une sorte.
Le but des classes virtuelles est plutôt de permettre le polymorphisme, c'est-à-dire que vous avez un pointeur inconnu sur un objet d'un type dérivé et lorsque vous appelez la fonction x, vous obtenez toujours la bonne fonction pour la classe sur laquelle pointe le pointeur de base.
Les pommes et les oranges vraiment.
Le problème le plus ennuyeux à propos du langage pimpl est qu’il est extrêmement difficile de maintenir et d’analyser le code existant. Donc, en utilisant pimpl, vous payez avec le temps et la frustration des développeurs uniquement pour "réduire les temps et les dépendances de construction et minimiser l'exposition des en-têtes aux détails de la mise en oeuvre". Décidez-vous, si cela en vaut vraiment la peine.
Le problème de "temps de construction" est un problème que vous pouvez résoudre en utilisant un meilleur matériel ou en utilisant des outils tels qu'Incredibuild (www.incredibuild.com, également inclus dans Visual Studio 2017), sans affecter votre conception logicielle. La conception du logiciel doit être généralement indépendante de la manière dont le logiciel est construit.