Come implementare correttamente il modello di metodo di fabbrica in C ++


329

C'è questa cosa in C ++ che mi ha fatto sentire a disagio per un bel po 'di tempo, perché onestamente non so come farlo, anche se sembra semplice:

Come posso implementare correttamente il metodo Factory in C ++?

Obiettivo: consentire al client di creare un'istanza di un oggetto utilizzando i metodi di fabbrica anziché i costruttori dell'oggetto, senza conseguenze inaccettabili e un impatto sulle prestazioni.

Per "modello di metodo di fabbrica" ​​intendo sia metodi di fabbrica statici all'interno di un oggetto o metodi definiti in un'altra classe, sia funzioni globali. In genere "il concetto di reindirizzare il modo normale di istanziazione della classe X in qualsiasi altro luogo rispetto al costruttore".

Lasciami sfogliare alcune possibili risposte a cui ho pensato.


0) Non creare fabbriche, creare costruttori.

Sembra bello (e in effetti spesso la soluzione migliore), ma non è un rimedio generale. Prima di tutto, ci sono casi in cui la costruzione di oggetti è un'attività abbastanza complessa da giustificare la sua estrazione in un'altra classe. Ma anche mettere da parte questo fatto, anche per oggetti semplici che usano solo costruttori spesso non lo faranno.

L'esempio più semplice che conosco è una classe Vector 2D. Così semplice, ma difficile. Voglio essere in grado di costruirlo sia da coordinate cartesiane che polari. Ovviamente, non posso fare:

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

Il mio modo di pensare naturale è quindi:

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    // ...
};

Il che, invece dei costruttori, mi porta all'utilizzo di metodi statici di fabbrica ... il che significa essenzialmente che sto implementando il modello di fabbrica, in qualche modo ("la classe diventa la propria fabbrica"). Sembra bello (e si adatterebbe a questo caso particolare), ma in alcuni casi fallisce, che descriverò al punto 2. Continua a leggere.

un altro caso: provare a sovraccaricare di due typedef opachi di alcune API (come GUID di domini non correlati, o un GUID e un campo di bit), tipi semanticamente totalmente diversi (quindi - in teoria - sovraccarichi validi) ma che in realtà si rivelano essere i stessa cosa - come ints senza segno o puntatori vuoti.


1) The Java Way

Java è semplice, poiché abbiamo solo oggetti allocati dinamici. Fare una fabbrica è banale come:

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);
    }
}

In C ++, questo si traduce in:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

Freddo? Spesso, infatti. Ma poi, questo costringe l'utente a utilizzare solo l'allocazione dinamica. L'allocazione statica è ciò che rende complesso il C ++, ma è anche ciò che spesso lo rende potente. Inoltre, credo che esistano alcuni obiettivi (parola chiave: incorporato) che non consentono l'allocazione dinamica. E ciò non implica che agli utenti di quelle piattaforme piace scrivere OOP pulito.

Comunque, filosofia a parte: nel caso generale, non voglio costringere gli utenti della fabbrica a essere limitati all'allocazione dinamica.


2) Ritorno per valore

OK, quindi sappiamo che 1) va bene quando vogliamo un'allocazione dinamica. Perché non aggiungeremo l'allocazione statica?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};

Che cosa? Non possiamo sovraccaricare il tipo restituito? Oh, certo che non possiamo. Quindi cambiamo i nomi dei metodi per riflettere quello. E sì, ho scritto l'esempio di codice non valido sopra solo per sottolineare quanto non mi piace la necessità di cambiare il nome del metodo, ad esempio perché non possiamo implementare correttamente un design di fabbrica indipendente dalla lingua, poiché dobbiamo cambiare i nomi - e ogni utente di questo codice dovrà ricordare quella differenza dell'implementazione dalla specifica.

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};

OK ... eccolo qui. È brutto, poiché dobbiamo cambiare il nome del metodo. È imperfetto, dal momento che dobbiamo scrivere due volte lo stesso codice. Ma una volta fatto, funziona. Destra?

Bene, di solito. Ma a volte no. Quando creiamo Foo, in realtà dipendiamo dal compilatore per fare l'ottimizzazione del valore di ritorno per noi, perché lo standard C ++ è abbastanza benevolo per i fornitori del compilatore da non specificare quando l'oggetto verrà creato sul posto e quando verrà copiato quando restituisce un oggetto temporaneo per valore in C ++. Quindi, se Foo è costoso da copiare, questo approccio è rischioso.

E se Foo non fosse affatto copiabile? Bene, doh. ( Si noti che in C ++ 17 con elision copia garantita, il non essere copiabile non è più un problema per il codice sopra )

Conclusione: fare una fabbrica restituendo un oggetto è davvero una soluzione per alcuni casi (come il vettore 2D precedentemente citato), ma non è ancora una sostituzione generale per i costruttori.


3) Costruzione bifase

Un'altra cosa che probabilmente verrebbe fuori è la separazione della questione dell'allocazione degli oggetti e della sua inizializzazione. Questo di solito si traduce in codice come questo:

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());
    // ...
}

Si potrebbe pensare che funzioni come un fascino. L'unico prezzo che paghiamo nel nostro codice ...

Da quando ho scritto tutto questo e lasciato questo come ultimo, devo anche non gradirmi. :) Perché?

Prima di tutto ... Non mi piace sinceramente il concetto di costruzione in due fasi e mi sento in colpa quando lo uso. Se progetto i miei oggetti con l'affermazione che "se esiste, è in uno stato valido", sento che il mio codice è più sicuro e meno soggetto a errori. Mi piace così.

Dover abbandonare quella convenzione E cambiare il design del mio oggetto solo allo scopo di farne una fabbrica è ... beh, ingombrante.

So che quanto sopra non convincerà molte persone, quindi lasciami dare alcuni argomenti più solidi. Utilizzando la costruzione a due fasi, non è possibile:

  • inizializza consto fa riferimento alle variabili dei membri,
  • passare argomenti ai costruttori di classi di base e ai costruttori di oggetti membro.

E probabilmente potrebbero esserci altri svantaggi a cui non riesco a pensare in questo momento, e non mi sento nemmeno particolarmente obbligato poiché i punti elenco sopra citati mi convincono già.

Quindi: nemmeno vicino a una buona soluzione generale per l'implementazione di una fabbrica.


conclusioni:

Vogliamo avere un modo di creare un'istanza dell'oggetto che:

  • consentire un'istanza uniforme indipendentemente dall'allocazione,
  • dare nomi diversi e significativi ai metodi di costruzione (quindi non fare affidamento sul sovraccarico per argomento),
  • non introdurre un significativo aumento delle prestazioni e, preferibilmente, un notevole aumento del codice, soprattutto sul lato client,
  • essere generale, come in: possibile essere introdotto per qualsiasi classe.

Credo di aver dimostrato che i modi che ho citato non soddisfano tali requisiti.

Qualche suggerimento? Per favore, forniscimi una soluzione, non voglio pensare che questo linguaggio non mi permetta di implementare correttamente un concetto così banale.


7
@Zac, anche se il titolo è molto simile, le domande reali sono diverse dall'IMHO.
Péter Török,

2
Buon duplicato, ma il testo di questa domanda è prezioso in sé e per sé.
dmckee --- ex gattino moderatore,

7
Due anni dopo averlo fatto, ho alcuni punti da aggiungere: 1) Questa domanda è rilevante per diversi modelli di progettazione (fabbrica [astratta], costruttore, nome, non mi piace approfondire la loro tassonomia). 2) Il vero problema in discussione qui è "come disaccoppiare in modo pulito l'allocazione della memoria degli oggetti dalla costruzione degli oggetti?".
Kos,

1
@Dennis: solo se non deletelo fai . Questo tipo di metodi va benissimo, purché sia ​​"documentato" (il codice sorgente è la documentazione ;-)) che il chiamante assume la proprietà del puntatore (leggi: è responsabile dell'eliminazione quando appropriato).
Boris Dalstein,

1
@Boris @Dennis potresti anche renderlo molto esplicito restituendo un unique_ptr<T>invece di T*.
Kos,

Risposte:


107

Prima di tutto, ci sono casi in cui la costruzione di oggetti è un'attività abbastanza complessa da giustificare la sua estrazione in un'altra classe.

Credo che questo punto sia errato. La complessità non ha molta importanza. La pertinenza è ciò che fa. Se un oggetto può essere costruito in un solo passaggio (non come nel modello del costruttore), il costruttore è il posto giusto per farlo. Se hai davvero bisogno di un'altra classe per eseguire il lavoro, allora dovrebbe essere comunque una classe helper utilizzata dal costruttore.

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

C'è una soluzione semplice per questo:

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

L'unico svantaggio è che sembra un po 'prolisso:

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

Ma la cosa buona è che puoi vedere immediatamente quale tipo di coordinate stai usando e allo stesso tempo non devi preoccuparti di copiare. Se vuoi copiare, ed è costoso (come dimostrato dalla profilazione, ovviamente), potresti voler usare qualcosa come le classi condivise di Qt per evitare di copiare le spese generali.

Per quanto riguarda il tipo di allocazione, il motivo principale per utilizzare il modello di fabbrica è solitamente il polimorfismo. I costruttori non possono essere virtuali e, anche se potessero, non avrebbe molto senso. Quando si utilizza l'allocazione statica o dello stack, non è possibile creare oggetti in modo polimorfico perché il compilatore deve conoscere la dimensione esatta. Quindi funziona solo con puntatori e riferimenti. E restituire un riferimento da una fabbrica non funziona troppo, perché mentre un oggetto può essere tecnicamente eliminato per riferimento, potrebbe essere piuttosto confuso e soggetto a bug, vedi La pratica di restituire una variabile di riferimento C ++, male?per esempio. Quindi i puntatori sono l'unica cosa che rimane e che include anche i puntatori intelligenti. In altre parole, le fabbriche sono più utili se utilizzate con allocazione dinamica, quindi puoi fare cose del genere:

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

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

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

In altri casi, le fabbriche aiutano solo a risolvere problemi minori come quelli con sovraccarichi che hai citato. Sarebbe bello se fosse possibile usarli in modo uniforme, ma non fa molto male che è probabilmente impossibile.


21
+1 per le strutture cartesiane e polari. In genere è meglio creare classi e strutture che rappresentano direttamente i dati a cui sono destinati (al contrario di una struttura Vec generale). Anche la tua fabbrica è un buon esempio, ma il tuo esempio non illustra chi possiede il puntatore "a". Se la Factory 'f' lo possiede, probabilmente verrà distrutta quando 'f' lascia l'ambito, ma se 'f' non lo possiede, è importante che lo sviluppatore ricordi di liberare quella memoria o altrimenti una perdita di memoria può si verificano.
David Peterson,

1
Ovviamente un oggetto può essere cancellato per riferimento! Vedi stackoverflow.com/a/752699/404734 Questo ovviamente solleva la questione se è saggio restituire la memoria dinamica per riferimento, a causa del problema della potenziale assegnazione del valore restituito tramite copia (il chiamante potrebbe ovviamente fare anche qualcosa come int a = * ReturnsAPoninterToInt () e dovrebbe quindi affrontare lo stesso problema, se viene restituita memoria con memoria dinamica, come per i riferimenti, ma nella versione del puntatore l'utente deve dereferire esplicitamente invece di dimenticare di fare esplicitamente riferimento, per essere sbagliato) .
Kaiserludi,

1
@Kaiserludi, bel punto. Non ci avevo pensato, ma è ancora un modo "malvagio" di fare le cose. Modificato la mia risposta per riflettere ciò.
Sergei Tachenov,

Che dire di creare diverse classi non polimorfiche che sono immutabili? È quindi appropriato utilizzare un modello di fabbrica in C ++?
daaxix,

@daaxix, perché avresti bisogno di una factory per creare istanze di una classe non polimorfica? Non vedo cosa abbia a che fare l'immutabilità con nulla di tutto ciò.
Sergei Tachenov,

49

Esempio semplice di fabbrica:

// 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();
    }
};

2
@LokiAstari Perché l'uso di puntatori intelligenti è il modo più semplice per perdere il controllo sulla memoria. Il controllo di quali lang C / C ++ sono noti per essere supremo rispetto ad altre lingue e da cui ottengono il massimo vantaggio. Per non parlare del fatto che i puntatori intelligenti producono un sovraccarico di memoria simile ad altre lingue gestite. Se vuoi la comodità della gestione automatica della memoria, inizia a programmare in Java o C # ma non mettere quel casino in C / C ++.
luke1985,

45
@ lukasz1985 unique_ptrin questo esempio non ha un sovraccarico prestazionale. La gestione delle risorse, compresa la memoria, è uno dei vantaggi supremi del C ++ rispetto a qualsiasi altra lingua perché è possibile farlo senza penalità di prestazione e in modo deterministico, senza perdere il controllo, ma si dice esattamente il contrario. Ad alcune persone non piacciono le cose implicite in C ++, come la gestione della memoria tramite puntatori intelligenti, ma se quello che vuoi è che tutto sia esplicitamente obbligatorio, usa C; il compromesso è ordini di grandezza meno problemi. Penso che sia ingiusto che tu voti una buona raccomandazione.
TheCppZoo,

1
@EdMaster: non ho risposto prima perché ovviamente stava trollando. Per favore, non dare da mangiare al troll.
Martin York,

17
@LokiAstari potrebbe essere un troll, ma ciò che dice potrebbe confondere le persone
TheCppZoo

1
@yau: Sì. Ma: boost::ptr_vector<>è un po 'più efficiente in quanto comprende che possiede il puntatore anziché delegare il lavoro a una sottoclasse. MA il vantaggio principale boost::ptr_vector<>è che espone i suoi membri per riferimento (non puntatore), quindi è davvero facile da usare con gli algoritmi nella libreria standard.
Martin York,

41

Hai mai pensato di non utilizzare affatto una fabbrica e di fare invece un buon uso del sistema di tipi? Posso pensare a due approcci diversi che fanno questo genere di cose:

Opzione 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) { /* ... */ }
};

Che ti permette di scrivere cose come:

Vec2 v(linear(1.0, 2.0));

Opzione 2:

puoi usare "tag" come fa la STL con iteratori e simili. Per esempio:

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 &) { /* ... */ }
};

Questo secondo approccio consente di scrivere un codice simile al seguente:

Vec2 v(1.0, 2.0, linear_coord);

che è anche bello ed espressivo mentre ti permette di avere prototipi unici per ogni costruttore.


29

Puoi leggere un'ottima soluzione in: http://www.codeproject.com/Articles/363338/Factory-Pattern-in-Cplusplus

La soluzione migliore si trova nei "commenti e discussioni", vedere "Non sono necessari metodi di creazione statici".

Da questa idea, ho fatto una fabbrica. Nota che sto usando Qt, ma puoi cambiare QMap e QString per equivalenti standard.

#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

Esempio di utilizzo:

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");

17

Concordo principalmente con la risposta accettata, ma esiste un'opzione C ++ 11 che non è stata coperta nelle risposte esistenti:

  • Restituisce i risultati del metodo di fabbrica per valore e
  • Fornire un costruttore di mosse economico .

Esempio:

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

Quindi puoi costruire oggetti nello stack:

sandwich mine{sandwich::ham()};

Come oggetti secondari di altre cose:

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

O allocato dinamicamente:

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

Quando potrei usare questo?

Se, su un costruttore pubblico, non è possibile fornire inizializzatori significativi per tutti i membri della classe senza alcun calcolo preliminare, allora potrei convertire quel costruttore in un metodo statico. Il metodo statico esegue i calcoli preliminari, quindi restituisce un risultato di valore tramite un costruttore privato che esegue solo un'inizializzazione basata sul membro.

Dico " potrebbe " perché dipende da quale approccio fornisce il codice più chiaro senza essere inutilmente inefficiente.


1
L'ho usato ampiamente durante il wrapping delle risorse OpenGL. Costruttori di copie eliminati e assegnazione di copie forzando l'uso della semantica di spostamento. Ho quindi creato un gruppo di metodi statici di fabbrica per la creazione di ogni tipo di risorsa. Questo era molto più leggibile dell'invio di runtime basato su enum di OpenGL che spesso ha un sacco di parametri di funzione ridondanti a seconda dell'enum passato. È un modello molto utile, sorpreso che questa risposta non sia più in alto.
Fibre

11

Loki ha sia un metodo di fabbrica che una fabbrica astratta . Entrambi sono documentati (ampiamente) in Modern C ++ Design , da Andei Alexandrescu. Il metodo factory è probabilmente più vicino a quello che sembra essere dopo, sebbene sia ancora un po 'diverso (almeno se la memoria serve, richiede che tu registri un tipo prima che la factory possa creare oggetti di quel tipo).


1
Anche se è obsoleto (cosa che contendo), è ancora perfettamente funzionante. Uso ancora una Factory basata su MC ++ D in un nuovo progetto C ++ 14 con grande efficacia! Inoltre, i modelli Factory e Singleton sono probabilmente le parti meno obsolete. Mentre pezzi di Loki simili Functione le manipolazioni del tipo possono essere sostituiti con std::functione <type_traits>e mentre lambda, threading, riferimenti a valori hanno implicazioni che possono richiedere alcune piccole modifiche, non vi è alcun rimpiazzo standard per singleton di fabbriche mentre li descrive.
metallo

5

Non provo a rispondere a tutte le mie domande, poiché credo che sia troppo ampio. Solo un paio di note:

ci sono casi in cui la costruzione di oggetti è un'attività abbastanza complessa da giustificare la sua estrazione in un'altra classe.

Quella classe è in realtà un costruttore , piuttosto che una fabbrica.

In generale, non voglio costringere gli utenti della fabbrica a essere limitati all'allocazione dinamica.

Quindi potresti far incapsulare la tua fabbrica in un puntatore intelligente. Credo in questo modo che tu possa avere la tua torta e mangiarla anche tu.

Ciò elimina anche i problemi relativi al ritorno per valore.

Conclusione: fare una fabbrica restituendo un oggetto è davvero una soluzione per alcuni casi (come il vettore 2D precedentemente citato), ma non è ancora una sostituzione generale per i costruttori.

Infatti. Tutti i modelli di progettazione presentano vincoli e svantaggi (specifici della lingua). Si consiglia di usarli solo quando ti aiutano a risolvere il tuo problema, non per se stessi.

Se stai cercando l'implementazione "perfetta" in fabbrica, beh, buona fortuna.


Grazie per la risposta! Ma potresti spiegare come l'uso di un puntatore intelligente rilascerebbe la restrizione dell'allocazione dinamica? Non ho capito bene questa parte.
Kos,

@Kos, con i puntatori intelligenti puoi nascondere l'allocazione / deallocazione dell'oggetto reale ai tuoi utenti. Vedono solo il puntatore intelligente incapsulante, che verso il mondo esterno si comporta come un oggetto allocato staticamente.
Péter Török,

@Kos, non in senso stretto, AFAIR. Si passa l'oggetto da avvolgere, che probabilmente è stato allocato in modo dinamico a un certo punto. Quindi il puntatore intelligente ne prende la proprietà e assicura che venga correttamente distrutto quando non è più necessario (il cui tempo viene deciso in modo diverso per i diversi tipi di puntatori intelligenti).
Péter Török,

3

Questa è la mia soluzione in stile c ++ 11. il parametro 'base' è per la classe base di tutte le sottoclassi. i creatori, sono oggetti std :: function per creare istanze di sottoclassi, potrebbero essere un'associazione alla sottoclasse di "funzione membro statico" create (alcuni argomenti) ". Questo forse non è perfetto ma funziona per me. Ed è una specie di soluzione "generale".

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 esempio di utilizzo.

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();
  }
}

Mi sembra carino. Come implementeresti (forse qualche macro magia) la registrazione statica? Immagina semplicemente che la classe base sia una classe di manutenzione per gli oggetti. Le classi derivate forniscono un tipo speciale di manutenzione a quegli oggetti. E vuoi aggiungere progressivamente diversi tipi di servizi aggiungendo una classe derivata dalla base per ciascuno di quei tipi di servizi.
St0fF,

2

Modello di fabbrica

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

E se il compilatore non supporta l'ottimizzazione del valore di ritorno, abbandonalo, probabilmente non contiene molta ottimizzazione ...


Questo può davvero essere considerato un'implementazione del modello di fabbrica?
Dennis,

1
@Dennis: come caso degenerato, la penso così. Il problema Factoryè che è abbastanza generico e copre molto terreno; una fabbrica può aggiungere argomenti (a seconda dell'ambiente / impostazione) o fornire un po 'di cache (relativa a Flyweight / Pools) per esempio, ma questi casi hanno senso solo in alcune situazioni.
Matthieu M.

Se solo cambiare il compilatore fosse facile come lo fai sembrare :)
rozina

@rozina: :) Funziona bene in Linux (gcc / clang sono notevolmente compatibili); Ammetto che Windows è ancora relativamente chiuso, anche se dovrebbe migliorare su piattaforma a 64 bit (meno brevetti sulla strada, se ricordo bene).
Matthieu M.

E poi hai tutto il mondo incorporato con alcuni compilatori subpar .. :) Sto lavorando con uno come quello che non ha l'ottimizzazione del valore di ritorno. Mi piacerebbe averlo fatto. Sfortunatamente il passaggio non è un'opzione in questo momento. Spero che in futuro verrà aggiornato o faremo un passaggio per altro :)
rozina

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.