Enregistrer des classes dans une Factory

Mais le faire sans action de l'utilisateur !

a marqué ce sujet comme résolu.

Alors, pour un projet perso, j'ai besoin d'une factory dans un cadre de polymorphisme. J'ai donc quelque chose comme :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Format {};

// Il manque tous les tests et les exceptions en cas d'erreur, mais l'idée est là.
class Factory {
    typedef unique_ptr<Format> (*format_creator_t) ();
    typedef std::unordered_map<std::string, format_creator_t> format_map_t;
public:
    static Factory formats() {
        static format_map_t map;
        return map;
    }
    static bool reg(const std::string& key, format_creator_t val) {
        auto map = formats();
        map.emplace(key, val);
        return true;
    }
    static unique_ptr<Format> get(const std::string& key) {
        auto map = formats();
        return map[key]();
    }
};

Là dessus, j'aimerai bien que chaque format s'enregistre de lui-même dans la factory, sans que l'utilisateur n'ai besoin de le faire au début de son main. J'ai une variable de classe statique dans chaque pair .hpp/.cpp, qui se charge d'enregistrer mon Format.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class FormatA : public Format {
public:
    // ...
private:
    static bool registered;
};

static bool FormatA::registered = Factory::reg("format_a", [](){
    return std::unique_ptr<Format>(new FormatA());
});

Si vous voulez voir le code en vrai, la factory est , et un exemple de format ici et

Seulement, ce code m'oblige à utiliser une bibliothèque partagée, pour que les symboles correspondant ne soit pas supprimés par le linker, et que mes formats soit bien enregistré à l'initialisation de la bibliothèque.

Je me demandais donc s'il pouvait y avoir une autre astuce en C++ pour gérer ce genre de cas, et me permettre de proposer une bibliothèque statique à mes utilisateur. Toutes les implémentations de Factory que j'ai vu çà ce jour utilisent un enregistrement dans le main de l'appelant.

Une solution serait d'avoir une fonction init_formats, que l'utilisateur se charge appeler au début de son utilisation, et qui effectue cet enregistrement. Seulement, cela me force à mettre à jour le code de cette fonction à chaque ajout de format, au lieu d'avoir simplement une ligne en bas du .cpp correspondant au format. Ces deux points me gênent un peu.

+0 -0

Lu'!

Quelques commentaires et notes sur ton code (celui des liens) avant de répondre au reste :

  • plutôt que typedef, on préférera utiliser l'écriture plus générique "using" :
1
2
//template<class T>
using NomDeType = AutreType/*<T,42>*/;
  • Utilise "std::make_unique" (si tu n'as pas compilo supportant C++14, trouve l'implémentation sur le net, elle est facile à trouver) :
1
return std::make_unique<Type>(/*paramètres à passer au constructeur*/);
  • Tu utilises beaucoup de pointeurs nus en entrée de fonction sans même vérifier qu'ils sont valides, de plus du fait que tu les utilises quoi qu'il arrive, ça induit qu'ils vont vers des ressources non-optionnelles. Une référence serait plus indiquée
  • Généralement, les dynamic_casts sont le signe qu'il y a quelque chose de pas clair dans la conception, es-tu sûr que tu aies besoin de ça ? D'autre part, ils peuvent échouer, attention aux dégâts. Si tu es sûr du type que tu reçois, la conversion serait plutôt un static_cast, mais même comme ça, je te conseille de re-jeter un oeil à ta conception.

Ensuite concernant les factories. On a en fait a priori pas de raison particulière de la rendre complètement statique par défaut. Il est bien plus flexible de pouvoir déclarer des factories locales et de déterminer à ce moment là ce qu'on veut qu'elle puisse produire.

Si vraiment tu veux fournir à l'utilisateur une factory avec un certain set de classes particulier (voir un set configurable), tu peux utiliser un genre de Builder qui va être capable de renvoyer une factory toute belle. Tu peux même éventuellement le faire avec un fichier de configuration en entrée qui va se charger de créer tout ça. Enfin, pour que ce builder connaisse toutes les factories. Tu peux lui donner un champ statique auprès duquel chaque classe souhaitant pouvoir être intégrée à la liste des builders va s'enregistrer par la création d'un élément statique d'enregistrement. Par contre, gaffe aux fiasco sur l'initialisation, et à la présence d'une instance de factory dans le cas où c'est une classe :

1
2
3
4
5
//fichier header du builder:
class RegisterFunctor{
public:
  RegisterFunctor(id, factory_function);
};
1
2
//facto header :
Machin create();
1
static RegisterFunctor name(id, create);

Une référence serait plus indiquée

Une référence peut être polymorphique ? Genre une référence vers BasicFile passée à une fonction ayant comme paramètre File& (avec class BasicFile : File) ? Parce que c'est pour ça que j'utilise des pointeurs !

Mais je pensais en effet modifier un poil la conception, pour éviter de passer un pointeur à chaque appel de fonction.

Généralement, les dynamic_casts sont le signe qu'il y a quelque chose de pas clair dans la conception, es-tu sûr que tu aies besoin de ça ? D'autre part, ils peuvent échouer, attention aux dégâts. Si tu es sûr du type que tu reçois, la conversion serait plutôt un static_cast, mais même comme ça, je te conseille de re-jeter un oeil à ta conception.

De mémoire, j'avais des erreurs de compilation en faisant des static_cast de pointeurs vers pointeurs. Je vais vérifier.

Tu peux même éventuellement le faire avec un fichier de configuration en entrée qui va se charger de créer tout ça.

Le fichier de conf est inutile ici, la factory est unique pour tout le programme et le set de classes est déterminé à la compilation. Elle est statique de partout car c'est un singleton. Le problème avec le builder ici, c'est que je dois modifier le code du builder pour ajouter une nouvelle classe, et pas simplement le code de la classe. Si c'est la seule solution, je ferais sans doutes ça, mais je préfèrerai éviter.

Tu peux lui donner un champ statique auprès duquel chaque classe souhaitant pouvoir être intégrée à la liste des builders va s'enregistrer par la création d'un élément statique d'enregistrement.

Je n'ai pas compris ce passage. À quel endroit se fait l'enregistrement ? Dans le fichier de la classe ? Les variables statiques non utilisée ne se font pas dégager par le linker ?

+0 -0

Avec boost::mpl.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <iostream>
#include <map>
#include <functional>
#include <memory>
#include <unordered_map>
#include <string>
#include <typeinfo>
#include <boost/mpl/for_each.hpp>
#include <boost/mpl/vector.hpp>
using namespace boost;
using namespace std;

struct F{
    virtual char const * const name() const =0;
};

struct Factory{
    using format_creator_t =  std::function<unique_ptr<F>()>;   
    using format_map_t = std::unordered_map<std::string, format_creator_t> ;
public:
    static format_map_t& formats() {
        static format_map_t map;
        return map;
    }

    static bool reg(const std::string& key, format_creator_t val) {
        auto& map = formats();
        map.emplace(key, val);
        return true;
    }
    static unique_ptr<F> get(const std::string& key) {
        auto map = formats();
        return map[key]();
    }
};
//#x va stringifier le token. ie _namer(ABC)
// va donner "ABC" 
#define _namer(x)  virtual char const * const name() const {static constexpr char s[] = #x;return s;}

struct A : F {     
    _namer(A)
};
struct B : F{
    _namer(B)
};
struct C : F{
    _namer(C)
};

using toRegister  = boost::mpl::vector<A,B,C> ;


struct registar
{
    Factory& fac;
    registar(Factory& f):fac(f){}
    template< typename U > void operator()(U x)
    {
    fac.reg(x.name(),[](){return std::make_unique<U>();});
    }

};

int main(int argc, char *argv[])
{
    Factory f;
    mpl::for_each<toRegister>(registar(f));
    auto a = f.get("B");
    std::cout<<a->name()<<std::endl;
    return 0;
} 

Le seul truc à faire, c'est :

  • Utiliser _namer dans ta classe
  • Ajouter la classe dans toRegister

Ce code va instancier 3 objets (un de chaque type). Tu peux éviter ca

On peut dégeulassifier la procédure avec un hack et construire toRegister après chaque classe mais je trouve ca plus clair de tout mettre un point.

+1 -0

J'aime bien cette solution !

On peut dégeulassifier la procédure avec un hack et construire toRegister après chaque classe mais je trouve ca plus clair de tout mettre un point.

C'est effectivement plus clair, mais ça nécessite d'inclure A.h, B.h et C.h dans Factory.h, et de mettre à jour un vecteur à chaque modification du code. Il faudrait que je regarde de plus près tout ça.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte