Comment implémenter correctement le modèle de méthode d'usine en C ++

329

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 constou 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.

Kos
la source
7
@Zac, bien que le titre soit très similaire, les questions réelles sont à mon humble avis différentes.
Péter Török, le
2
Bon doublon mais le texte de cette question est précieux en soi.
dmckee --- chaton ex-modérateur
7
Deux ans après avoir posé cette question, j'ai quelques points à ajouter: 1) Cette question concerne plusieurs modèles de conception (usine [abstraite], constructeur, vous l'appelez, je n'aime pas fouiller dans leur taxonomie). 2) Le problème réel discuté ici est "comment dissocier proprement l'allocation de stockage d'objets de la construction d'objets?".
Kos
1
@Dennis: seulement si vous ne le faites pas 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).
Boris Dalstein
1
@Boris @Dennis, vous pouvez également le rendre très explicite en renvoyant un unique_ptr<T>au lieu de T*.
Kos

Réponses:

107

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.

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.

Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!

Il existe une solution simple pour cela:

struct Cartesian {
  inline Cartesian(float x, float y): x(x), y(y) {}
  float x, y;
};
struct Polar {
  inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
  float angle, magnitude;
};
Vec2(const Cartesian &cartesian);
Vec2(const Polar &polar);

Le seul inconvénient est qu'il semble un peu bavard:

Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));

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:

class Abstract {
  public:
    virtual void do() = 0;
};

class Factory {
  public:
    Abstract *create();
};

Factory f;
Abstract *a = f.create();
a->do();

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.

Sergei Tachenov
la source
21
+1 pour les structures cartésiennes et polaires. Il est généralement préférable de créer des classes et des structures qui représentent directement les données auxquelles elles sont destinées (par opposition à une structure Vec générale). Votre usine est également un bon exemple, mais votre exemple n'illustre pas à qui appartient le pointeur «a». Si Factory 'f' en est propriétaire, il sera probablement détruit lorsque 'f' quittera la portée, mais si 'f' ne le possède pas, il est important que le développeur se souvienne de libérer cette mémoire, sinon une fuite de mémoire peut se produire.
David Peterson
1
Bien sûr, un objet peut être supprimé par référence! Voir stackoverflow.com/a/752699/404734 Cela soulève bien sûr la question de savoir s'il est judicieux de renvoyer la mémoire dynamique par référence, en raison du problème de l'attribution potentielle de la valeur de retour par copie (l'appelant pourrait bien sûr également faire quelque chose comme int a = * renvoieAPoninterToInt () et serait alors confronté au même problème, si la mémoire allouée dynamiquement est retournée, comme pour les références, mais dans la version du pointeur, l'utilisateur doit explicitement déréférencer au lieu d'oublier de faire explicitement référence, pour se tromper) .
Kaiserludi
1
@Kaiserludi, bon point. Je n'y ai pas pensé, mais c'est toujours une façon "diabolique" de faire les choses. Modifié ma réponse pour refléter cela.
Sergei Tachenov
Qu'en est-il de la création de différentes classes non polymorphes immuables? Un modèle d'usine est-il alors approprié à utiliser en C ++?
daaxix
@daaxix, pourquoi auriez-vous besoin d'une fabrique pour créer des instances d'une classe non polymorphe? Je ne vois pas ce que l'immuabilité a à voir avec quoi que ce soit de tout cela.
Sergei Tachenov
49

Exemple simple d'usine:

// Factory returns object and ownership
// Caller responsible for deletion.
#include <memory>
class FactoryReleaseOwnership{
  public:
    std::unique_ptr<Foo> createFooInSomeWay(){
      return std::unique_ptr<Foo>(new Foo(some, args));
    }
};

// Factory retains object ownership
// Thus returning a reference.
#include <boost/ptr_container/ptr_vector.hpp>
class FactoryRetainOwnership{
  boost::ptr_vector<Foo>  myFoo;
  public:
    Foo& createFooInSomeWay(){
      // Must take care that factory last longer than all references.
      // Could make myFoo static so it last as long as the application.
      myFoo.push_back(new Foo(some, args));
      return myFoo.back();
    }
};
Martin York
la source
2
@LokiAstari Parce que l'utilisation de pointeurs intelligents est le moyen le plus simple de perdre le contrôle de la mémoire. Le contrôle dont les langages C / C ++ sont connus pour être suprêmes par rapport aux autres langages, et dont ils tirent le plus grand avantage. Sans parler du fait que les pointeurs intelligents produisent une surcharge de mémoire similaire à d'autres langages gérés. Si vous voulez la commodité de la gestion automatique de la mémoire, commencez à programmer en Java ou en C # mais ne mettez pas ce désordre en C / C ++.
luke1985
45
@ lukasz1985 le unique_ptrdans 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.
TheCppZoo
1
@EdMaster: Je n'ai pas répondu auparavant car il était évidemment à la traîne. Veuillez ne pas nourrir le troll.
Martin York
17
@LokiAstari, il pourrait être un troll, mais ce qu'il dit pourrait embrouiller les gens
TheCppZoo
1
@yau: Oui. Mais: 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 de boost::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.
Martin York
41

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:

struct linear {
    linear(float x, float y) : x_(x), y_(y){}
    float x_;
    float y_;
};

struct polar {
    polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
    float angle_;
    float magnitude_;
};


struct Vec2 {
    explicit Vec2(const linear &l) { /* ... */ }
    explicit Vec2(const polar &p) { /* ... */ }
};

Ce qui vous permet d'écrire des choses comme:

Vec2 v(linear(1.0, 2.0));

Option 2:

vous pouvez utiliser des "balises" comme le fait la STL avec les itérateurs et autres. Par exemple:

struct linear_coord_tag linear_coord {}; // declare type and a global
struct polar_coord_tag polar_coord {};

struct Vec2 {
    Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
    Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
};

Cette deuxième approche vous permet d'écrire du code qui ressemble à ceci:

Vec2 v(1.0, 2.0, linear_coord);

ce qui est aussi agréable et expressif tout en vous permettant d'avoir des prototypes uniques pour chaque constructeur.

Evan Teran
la source
29

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.

#ifndef FACTORY_H
#define FACTORY_H

#include <QMap>
#include <QString>

template <typename T>
class Factory
{
public:
    template <typename TDerived>
    void registerType(QString name)
    {
        static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
        _createFuncs[name] = &createFunc<TDerived>;
    }

    T* create(QString name) {
        typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
        if (it != _createFuncs.end()) {
            return it.value()();
        }
        return nullptr;
    }

private:
    template <typename TDerived>
    static T* createFunc()
    {
        return new TDerived();
    }

    typedef T* (*PCreateFunc)();
    QMap<QString,PCreateFunc> _createFuncs;
};

#endif // FACTORY_H

Exemple d'utilisation:

Factory<BaseClass> f;
f.registerType<Descendant1>("Descendant1");
f.registerType<Descendant2>("Descendant2");
Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
BaseClass *b1 = f.create("Descendant1");
BaseClass *b2 = f.create("Descendant2");
mabg
la source
17

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:

  • Renvoyer les résultats de la méthode d'usine par valeur , et
  • Fournir un constructeur de déménagement bon marché .

Exemple:

struct sandwich {
  // Factory methods.
  static sandwich ham();
  static sandwich spam();
  // Move constructor.
  sandwich(sandwich &&);
  // etc.
};

Ensuite, vous pouvez construire des objets sur la pile:

sandwich mine{sandwich::ham()};

Comme sous-objets d'autres choses:

auto lunch = std::make_pair(sandwich::spam(), apple{});

Ou alloués dynamiquement:

auto ptr = std::make_shared<sandwich>(sandwich::ham());

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.

mbrcknl
la source
1
Je l'ai beaucoup utilisé lors de l'encapsulation des ressources OpenGL. Constructeurs de copie supprimés et affectation de copie forçant l'utilisation de la sémantique de déplacement. J'ai ensuite créé un tas de méthodes d'usine statiques pour créer chaque type de ressource. Cela était beaucoup plus lisible que la répartition d'exécution basée sur l'énumération d'OpenGL qui a souvent un tas de paramètres de fonction redondants en fonction de l'énumération passée. C'est un modèle très utile, surpris que cette réponse ne soit pas plus élevée.
Fibbles
11

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).

Jerry Coffin
la source
1
Même s'il est obsolète (ce que je conteste), il est toujours parfaitement utilisable. J'utilise toujours une usine basée sur MC ++ D dans un nouveau projet C ++ 14 pour un grand effet! De plus, les motifs Factory et Singleton sont probablement les pièces les moins périmées. Alors que des morceaux de Loki aiment Functionet les manipulations de type peuvent être remplacés par std::functionet <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.
metal
5

Je n'essaie pas de répondre à toutes mes questions, car je pense que c'est trop large. Juste quelques notes:

il y a des cas où la construction d'objets est une tâche suffisamment complexe pour justifier son extraction dans une autre classe.

Cette classe est en fait un constructeur , plutôt qu'une usine.

Dans le cas général, je ne veux pas forcer les utilisateurs de l'usine à être restreints à l'allocation dynamique.

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.

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.

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.

Péter Török
la source
Merci d'avoir répondu! Mais pourriez-vous expliquer comment l'utilisation d'un pointeur intelligent libérerait la restriction de l'allocation dynamique? Je n'ai pas bien compris cette partie.
Kos
@Kos, avec des pointeurs intelligents, vous pouvez masquer l'allocation / la désallocation de l'objet réel à vos utilisateurs. Ils ne voient que le pointeur intelligent encapsulant, qui, pour le monde extérieur, se comporte comme un objet alloué statiquement.
Péter Török
@Kos, pas au sens strict, AFAIR. Vous passez l'objet à envelopper, que vous avez probablement alloué dynamiquement à un moment donné. Ensuite, le pointeur intelligent en prend possession et s'assure qu'il est correctement détruit lorsqu'il n'est plus nécessaire (dont le temps est décidé différemment pour différents types de pointeurs intelligents).
Péter Török
3

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».

template <class base, class... params> class factory {
public:
  factory() {}
  factory(const factory &) = delete;
  factory &operator=(const factory &) = delete;

  auto create(const std::string name, params... args) {
    auto key = your_hash_func(name.c_str(), name.size());
    return std::move(create(key, args...));
  }

  auto create(key_t key, params... args) {
    std::unique_ptr<base> obj{creators_[key](args...)};
    return obj;
  }

  void register_creator(const std::string name,
                        std::function<base *(params...)> &&creator) {
    auto key = your_hash_func(name.c_str(), name.size());
    creators_[key] = std::move(creator);
  }

protected:
  std::unordered_map<key_t, std::function<base *(params...)>> creators_;
};

Un exemple d'utilisation.

class base {
public:
  base(int val) : val_(val) {}

  virtual ~base() { std::cout << "base destroyed\n"; }

protected:
  int val_ = 0;
};

class foo : public base {
public:
  foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }

  static foo *create(int val) { return new foo(val); }

  virtual ~foo() { std::cout << "foo destroyed\n"; }
};

class bar : public base {
public:
  bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }

  static bar *create(int val) { return new bar(val); }

  virtual ~bar() { std::cout << "bar destroyed\n"; }
};

int main() {
  common::factory<base, int> factory;

  auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
  auto bar_creator = std::bind(&bar::create, std::placeholders::_1);

  factory.register_creator("foo", foo_creator);
  factory.register_creator("bar", bar_creator);

  {
    auto foo_obj = std::move(factory.create("foo", 80));
    foo_obj.reset();
  }

  {
    auto bar_obj = std::move(factory.create("bar", 90));
    bar_obj.reset();
  }
}
DAG
la source
Ça m'a l'air bien. Comment implémenteriez-vous (peut-être une macro magique) un enregistrement statique? Imaginez simplement que la classe de base est une classe de service pour les objets. Les classes dérivées fournissent un type spécial de service à ces objets. Et vous souhaitez ajouter progressivement différents types de services en ajoutant une classe dérivée de la base pour chacun de ces types de services.
St0fF
2

Modèle d'usine

class Point
{
public:
  static Point Cartesian(double x, double y);
private:
};

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 ...

Matthieu M.
la source
Cela peut-il vraiment être considéré comme une implémentation du modèle d'usine?
Dennis
1
@ Dennis: En tant que cas dégénéré, je pense que oui. Le problème Factoryest 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.
Matthieu M.
Si seulement changer le compilateur serait aussi simple que vous le faites sonner :)
rozina
@rozina: :) Cela fonctionne bien sous Linux (gcc / clang sont remarquablement compatibles); J'admets que Windows est encore relativement fermé, bien qu'il devrait s'améliorer sur la plate-forme 64 bits (moins de brevets sur le chemin, si je me souviens bien).
Matthieu M.
Et puis vous avez tout le monde embarqué avec quelques compilateurs subpar .. :) Je travaille avec un comme celui qui n'a pas d'optimisation de la valeur de retour. J'aurais aimé que ce soit le cas. Malheureusement, le changement n'est pas une option pour le moment. Espérons que dans le futur, il sera mis à jour ou nous ferons un changement pour qq d'autre :)
rozina