Il y a une chose en C ++ qui me met mal à l'aise depuis assez longtemps, car honnêtement, je ne sais pas comment le faire, même si cela semble simple:
Comment implémenter correctement la méthode d'usine en C ++?
Objectif: permettre au client d'instancier un objet en utilisant des méthodes d'usine au lieu des constructeurs de l'objet, sans conséquences inacceptables et sans impact sur les performances.
Par «modèle de méthode d'usine», je veux dire à la fois des méthodes d'usine statiques à l'intérieur d'un objet ou des méthodes définies dans une autre classe, ou des fonctions globales. Généralement, "le concept de redirection de la manière normale d'instanciation de la classe X vers n'importe où ailleurs que le constructeur".
Permettez-moi de parcourir quelques réponses possibles auxquelles j'ai pensé.
0) Ne faites pas d'usines, faites des constructeurs.
Cela semble agréable (et en effet souvent la meilleure solution), mais ce n'est pas un remède général. Tout d'abord, il existe des cas où la construction d'objets est une tâche suffisamment complexe pour justifier son extraction dans une autre classe. Mais même en mettant ce fait de côté, même pour des objets simples utilisant simplement des constructeurs ne suffira pas.
L'exemple le plus simple que je connaisse est une classe de vecteur 2D. Si simple, mais délicat. Je veux pouvoir le construire à la fois à partir des coordonnées cartésiennes et polaires. Évidemment, je ne peux pas faire:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Ma façon de penser naturelle est alors:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Ce qui, au lieu de constructeurs, m'amène à utiliser des méthodes d'usine statiques ... ce qui signifie essentiellement que j'implémente le modèle d'usine, d'une certaine manière ("la classe devient sa propre usine"). Cela a l'air bien (et conviendrait à ce cas particulier), mais échoue dans certains cas, que je vais décrire au point 2. Continuez à lire.
un autre cas: essayer de surcharger par deux typedefs opaques de certaines API (comme les GUID de domaines non liés, ou un GUID et un champ de bits), des types sémantiquement totalement différents (donc - en théorie - des surcharges valides) mais qui s'avèrent en fait être les même chose - comme des entiers non signés ou des pointeurs vides.
1) La voie Java
Java est simple, car nous n'avons que des objets alloués dynamiquement. Faire une usine est aussi trivial que:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
En C ++, cela se traduit par:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Cool? Souvent, en effet. Mais alors, cela oblige l'utilisateur à n'utiliser que l'allocation dynamique. L'allocation statique est ce qui rend le C ++ complexe, mais c'est aussi ce qui le rend souvent puissant. De plus, je pense qu'il existe des cibles (mot-clé: intégré) qui ne permettent pas d'allocation dynamique. Et cela n'implique pas que les utilisateurs de ces plateformes aiment écrire des POO propres.
Quoi qu'il en soit, la philosophie mise à part: dans le cas général, je ne veux pas forcer les utilisateurs de l'usine à être restreints à l'allocation dynamique.
2) Retour par valeur
OK, nous savons donc que 1) est cool quand nous voulons une allocation dynamique. Pourquoi ne pas ajouter une allocation statique en plus de cela?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Quoi? Nous ne pouvons pas surcharger par le type de retour? Oh, bien sûr, nous ne pouvons pas. Modifions donc les noms de méthode pour refléter cela. Et oui, j'ai écrit l'exemple de code invalide ci-dessus juste pour souligner à quel point je n'aime pas la nécessité de changer le nom de la méthode, par exemple parce que nous ne pouvons pas implémenter correctement une conception d'usine indépendante du langage, car nous devons changer les noms - et chaque utilisateur de ce code devra se souvenir de cette différence d'implémentation par rapport à la spécification.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OK ... nous l'avons. C'est moche, car nous devons changer le nom de la méthode. C'est imparfait, car nous devons écrire deux fois le même code. Mais une fois terminé, cela fonctionne. Droite?
Enfin, d'habitude. Mais parfois non. Lors de la création de Foo, nous dépendons en fait du compilateur pour effectuer l'optimisation de la valeur de retour pour nous, car la norme C ++ est suffisamment bienveillante pour que les fournisseurs du compilateur ne spécifient pas quand l'objet créé en place et quand sera-t-il copié lors du retour d'un objet temporaire par valeur en C ++. Donc, si Foo coûte cher à copier, cette approche est risquée.
Et si Foo n'est pas du tout copiable? Et bien. ( Notez qu'en C ++ 17 avec élision de copie garantie, ne pas être copiable n'est plus un problème pour le code ci-dessus )
Conclusion: Faire une usine en retournant un objet est en effet une solution pour certains cas (comme le vecteur 2D précédemment mentionné), mais toujours pas un remplacement général pour les constructeurs.
3) Construction en deux phases
Une autre chose que quelqu'un trouverait probablement est de séparer la question de l'allocation des objets et de son initialisation. Cela se traduit généralement par un code comme celui-ci:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
On peut penser que cela fonctionne comme un charme. Le seul prix que nous payons dans notre code ...
Depuis que j'ai écrit tout cela et que je l'ai laissé comme dernier, je dois aussi ne pas l'aimer. :) Pourquoi?
Tout d'abord ... Je n'aime pas sincèrement le concept de construction en deux phases et je me sens coupable quand je l'utilise. Si je conçois mes objets en affirmant que "s'il existe, il est dans un état valide", je pense que mon code est plus sûr et moins sujet aux erreurs. Je l'aime comme ça.
Devoir abandonner cette convention ET changer la conception de mon objet dans le seul but d'en faire une usine est… enfin, lourd.
Je sais que ce qui précède ne convaincra pas beaucoup de gens, alors laissez-moi vous donner quelques arguments plus solides. En utilisant une construction en deux phases, vous ne pouvez pas:
- initialiser
const
ou référencer les variables membres, - passer des arguments aux constructeurs de classe de base et aux constructeurs d'objets membres.
Et il pourrait probablement y avoir d'autres inconvénients auxquels je ne peux pas penser en ce moment, et je ne me sens même pas particulièrement obligé de le faire car les points ci-dessus me convaincent déjà.
Donc: pas même proche d'une bonne solution générale pour l'implantation d'une usine.
Conclusions:
Nous voulons avoir un moyen d'instanciation d'objet qui:
- permettre une instanciation uniforme quelle que soit l'allocation,
- donner des noms différents et significatifs aux méthodes de construction (donc ne pas compter sur la surcharge par argument),
- ne pas introduire un hit significatif de performance et, de préférence, un hit significatif de ballonnement de code, en particulier côté client,
- être général, comme dans: peut être introduit pour n'importe quelle classe.
Je crois avoir prouvé que les moyens que j'ai mentionnés ne répondent pas à ces exigences.
Des indices? Veuillez me fournir une solution, je ne veux pas penser que ce langage ne me permettra pas de mettre correctement en œuvre un concept aussi trivial.
delete
. Ces types de méthodes sont parfaitement adaptés, tant qu'il est "documenté" (le code source est la documentation ;-)) que l'appelant s'approprie le pointeur (lire: est responsable de le supprimer le cas échéant).unique_ptr<T>
au lieu deT*
.Réponses:
Je pense que ce point est incorrect. La complexité n'a pas vraiment d'importance. La pertinence est ce qui fait. Si un objet peut être construit en une seule étape (pas comme dans le modèle de générateur), le constructeur est le bon endroit pour le faire. Si vous avez vraiment besoin d'une autre classe pour effectuer le travail, il doit quand même s'agir d'une classe auxiliaire utilisée par le constructeur.
Il existe une solution simple pour cela:
Le seul inconvénient est qu'il semble un peu bavard:
Mais la bonne chose est que vous pouvez immédiatement voir quel type de coordonnées vous utilisez, et en même temps vous n'avez pas à vous soucier de la copie. Si vous voulez copier et que cela coûte cher (comme le prouve le profilage, bien sûr), vous pouvez utiliser quelque chose comme les classes partagées de Qt pour éviter de copier les frais généraux.
Quant au type d'allocation, la principale raison d'utiliser le modèle d'usine est généralement le polymorphisme. Les constructeurs ne peuvent pas être virtuels, et même s'ils le pouvaient, cela n'aurait pas beaucoup de sens. Lorsque vous utilisez une allocation statique ou de pile, vous ne pouvez pas créer d'objets de manière polymorphe car le compilateur doit connaître la taille exacte. Il ne fonctionne donc qu'avec des pointeurs et des références. Et retourner une référence d'une usine ne fonctionne pas non plus, car même si un objet peut techniquement être supprimé par référence, il pourrait être assez déroutant et sujet aux bogues, voir Est-ce que la pratique de renvoyer une variable de référence C ++ est mauvaise?par exemple. Les pointeurs sont donc la seule chose qui reste, et cela inclut également les pointeurs intelligents. En d'autres termes, les usines sont plus utiles lorsqu'elles sont utilisées avec l'allocation dynamique, vous pouvez donc faire des choses comme ceci:
Dans d'autres cas, les usines aident simplement à résoudre des problèmes mineurs comme ceux avec des surcharges que vous avez mentionnés. Ce serait bien s'il était possible de les utiliser de manière uniforme, mais cela ne fait pas grand-chose que c'est probablement impossible.
la source
Exemple simple d'usine:
la source
unique_ptr
dans cet exemple n'a pas de surcharge de performances. La gestion des ressources, y compris la mémoire, est l'un des avantages suprêmes du C ++ par rapport à tout autre langage car vous pouvez le faire sans perte de performances et de manière déterministe, sans perdre le contrôle, mais vous dites exactement le contraire. Certaines personnes n'aiment pas les choses que C ++ fait implicitement, comme la gestion de la mémoire via des pointeurs intelligents, mais si ce que vous voulez, c'est que tout soit obligatoirement explicite, utilisez C; le compromis est de l'ordre de grandeur moins de problèmes. Je pense qu'il est injuste que vous votiez contre une bonne recommandation.boost::ptr_vector<>
est un peu plus efficace car il comprend qu'il possède le pointeur plutôt que de déléguer le travail à une sous-classe. MAIS le principal avantage deboost::ptr_vector<>
c'est qu'il expose ses membres par référence (pas de pointeur) donc il est vraiment facile à utiliser avec des algorithmes dans la bibliothèque standard.Avez-vous pensé à ne pas utiliser du tout une usine et à utiliser plutôt le système de type? Je peux penser à deux approches différentes qui font ce genre de chose:
Option 1:
Ce qui vous permet d'écrire des choses comme:
Option 2:
vous pouvez utiliser des "balises" comme le fait la STL avec les itérateurs et autres. Par exemple:
Cette deuxième approche vous permet d'écrire du code qui ressemble à ceci:
ce qui est aussi agréable et expressif tout en vous permettant d'avoir des prototypes uniques pour chaque constructeur.
la source
Vous pouvez lire une très bonne solution dans: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus
La meilleure solution est sur les "commentaires et discussions", voir "Pas besoin de méthodes de création statiques".
De cette idée, j'ai fait une usine. Notez que j'utilise Qt, mais vous pouvez modifier QMap et QString pour les équivalents std.
Exemple d'utilisation:
la source
Je suis principalement d'accord avec la réponse acceptée, mais il existe une option C ++ 11 qui n'a pas été couverte dans les réponses existantes:
Exemple:
Ensuite, vous pouvez construire des objets sur la pile:
Comme sous-objets d'autres choses:
Ou alloués dynamiquement:
Quand pourrais-je l'utiliser?
Si, sur un constructeur public, il n'est pas possible de donner des initialiseurs significatifs pour tous les membres de la classe sans un calcul préliminaire, alors je pourrais convertir ce constructeur en une méthode statique. La méthode statique effectue les calculs préliminaires, puis renvoie un résultat de valeur via un constructeur privé qui ne fait qu'une initialisation par membre.
Je dis « pourrait » car cela dépend de l'approche qui donne le code le plus clair sans être inutilement inefficace.
la source
Loki a à la fois une méthode d'usine et une usine abstraite . Les deux sont documentés (en détail) dans Modern C ++ Design , par Andei Alexandrescu. La méthode d'usine est probablement plus proche de ce que vous semblez rechercher, bien qu'elle soit toujours un peu différente (au moins si la mémoire est bonne, elle vous oblige à enregistrer un type avant que l'usine puisse créer des objets de ce type).
la source
Function
et les manipulations de type peuvent être remplacés parstd::function
et<type_traits>
et tandis que les lambdas, le filetage, les références rvalue ont des implications qui peuvent nécessiter quelques ajustements mineurs, il n'y a pas de remplacement standard pour les singletons d'usines comme il les décrit.Je n'essaie pas de répondre à toutes mes questions, car je pense que c'est trop large. Juste quelques notes:
Cette classe est en fait un constructeur , plutôt qu'une usine.
Ensuite, votre usine pourrait l'encapsuler dans un pointeur intelligent. Je crois que de cette façon, vous pouvez avoir votre gâteau et le manger aussi.
Cela élimine également les problèmes liés au rendement par valeur.
En effet. Tous les modèles de conception ont leurs contraintes et inconvénients (spécifiques au langage). Il est recommandé de les utiliser uniquement lorsqu'ils vous aident à résoudre votre problème, pas pour eux-mêmes.
Si vous recherchez l'implémentation d'usine "parfaite", alors bonne chance.
la source
Ceci est ma solution de style c ++ 11. le paramètre 'base' est pour la classe de base de toutes les sous-classes. créateurs, sont des objets std :: function pour créer des instances de sous-classe, peuvent être une liaison à votre sous-classe 'fonction membre statique' create (certains arguments) '. Ce n'est peut-être pas parfait mais fonctionne pour moi. Et c'est une solution un peu «générale».
Un exemple d'utilisation.
la source
Modèle d'usine
Et si votre compilateur ne prend pas en charge l'optimisation de la valeur de retour, abandonnez-le, il ne contient probablement pas beaucoup d'optimisation du tout ...
la source
Factory
est qu'il est assez générique et couvre beaucoup de terrain; une fabrique peut ajouter des arguments (selon l'environnement / la configuration) ou fournir une mise en cache (liée à Flyweight / Pools) par exemple, mais ces cas n'ont de sens que dans certaines situations.Je sais qu'il a été répondu à cette question il y a 3 ans, mais c'est peut-être ce que vous cherchiez.
Google a publié il y a quelques semaines une bibliothèque permettant des allocations d'objets dynamiques simples et flexibles. Le voici: http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html
la source