Fabbriche, vettori e puntatori intelligenti - Domanda di progettazione

5

Quindi, il mio codice aziendale ha bisogno di alcuni oggetti.
Non sa quanti oggetti ha bisogno e non conosce i tipi esatti (perché il polimorfismo è coinvolto).
Per me, questo suona per una buona ragione per andare in fabbrica.

Il mio codice ora appare come:

std::vector<AbstractBaseClass*> objectList;
Factory f;
objectList = f.create("path/to/config.txt");

Il prototipo del metodo di creazione della factory ha il seguente aspetto:

std::vector<AbstractBaseClass*> Factory ::create(std::string configFile)

Buone notizie, funziona!
La Factory legge la configurazione, quindi decide quanti oggetti creare e quali tipi di calcestruzzo hanno.

Bene, ho fatto molte ricerche ma non ho trovato un esempio di fabbrica che restituisce non un singolo oggetto ma un contenitore. Quindi, la mia domanda: è questo buono stile?

Penso di sì perché in questo modo l'intero processo di parsing / creazione è nascosto dalla logica del business. La logica sa solo che ha un contenitore con un mucchio di oggetti. Ma forse hai altre opinioni?
Nota: questo progetto è incentrato sull'apprendimento delle buone abitudini OOP.

Ok, immagina che questo approccio sia OK.
Ho imparato, i puntatori grezzi sono malvagi. (OK, non sono cattivi per definizione, ma cerco di evitarli).
Quindi, voglio passare ad alcuni indicatori intelligenti. Manca boost e C++11 su questa macchina Sto iniziando con auto_ptr .

OK, nuovo approccio:

std::vector< auto_ptr<AbstractBaseClass> > objectList;
Factory f;
objectList = f.create("path/to/config.txt");

E fabbrica:

std::vector< auto_ptr <AbstractBaseClass> > Factory ::create(std::string configFile)

Questo sembra malvagio.
E non viene compilato perché al momento sto ricevendo alcuni errori del compilatore STL pazzi.
Ma immagina che compili.
È buono questo?

Non ho mai visto un simile costrutto - una fabbrica che restituisce un contenitore di puntatori intelligenti. E poiché non sono un esperto, voglio chiederti cosa ne pensi.

Su una nota correlata, quale puntatore intelligente dovrei usare?
La logica aziendale è l'unico proprietario degli oggetti, quindi suppongo unique_ptr. Tuttavia, non sono sicuro che sia possibile restituire un contenitore unique_ptr. shared_ptr è più facile da implementare, credo.

    
posta lugge86 15.12.2015 - 16:23
fonte

2 risposte

9

Se usi C ++ come linguaggio OOP in gran parte, dovrai gestire i puntatori in un modo o nell'altro. La risposta a quasi tutti i problemi del puntatore è std::unique_ptr , perché ha una semantica abbastanza simile al valore pur continuando a consentire l'utilizzo del polimorfismo. Il suo unico sovraccarico è l'ingombro sintattico. Questo è molto meglio di

  • puntatori non elaborati, perché i puntatori non hanno una semantica della proprietà intrinseca. Ho visto puntatori usati sia per la semantica del prestito di riferimento, sia per il trasferimento di proprietà come copia. Questo potrebbe essere nella documentazione, ma certamente non è codificato nel codice sorgente o nel sistema di tipi.
  • il C ++ 11 std::shared_ptr perché questo ha un sovraccarico aggiuntivo per il conteggio dei riferimenti.
  • il deprecato std::auto_ptr perché ha semantics confuso: la copia si comporta come lo spostamento, che richiede anche che la sorgente di copia non sia const in modo che il puntatore spostato possa essere cancellato dall'origine. (Si noti inoltre che l'operatore di assegnazione della copia per std::vector si aspetta un riferimento const, quindi vector<auto_ptr<T>> non è copiabile.)
  • wrapper Pimpl scritti a mano, dal momento che può essere difficile implementare correttamente un tipo - ho visto abbastanza perdite di memoria e segfaults da implementazioni non corrette (suggerimento: rendere impossibile l'uso di un puntatore nullo come impl ). In effetti, raccomando che Pimpls sia implementato in termini di un campo unique_ptr privato poiché si occupa di tutta la gestione delle risorse necessaria.

Tuttavia, l'utilizzo di un Pimpl come Bridge Pattern può consentire di progettare una gerarchia di classi polimorfiche con un'API basata sul valore ma una semantica del puntatore. In particolare, potresti utilizzare vector<BaseClass> come notazione più comoda per vector<unique_ptr<BaseClassIf>> .

Il "problema" con std::unique_ptr è che richiede C ++ 11. L'idea principale di questo tipo di puntatore è che non può essere copiato, ma può essere spostato. Pertanto, la proprietà è sempre chiaramente definita. Quando restituisci un oggetto in base al valore, sarà soggetto a copiare la semantica (anche se la copia effettiva potrebbe essere ottimizzata), tranne in C ++ 11 dove la semantica del movimento ti consentirà di restituire un unique_ptr in base al valore.

In generale, usare un contenitore di puntatori intelligenti è perfettamente soddisfacente e migliore delle alternative. Nel tuo caso, fallisce a causa della semantica ristretta di C ++ 03 rispetto a auto_ptr . Se non è possibile eseguire l'aggiornamento a C ++ 11 (che è supportato in tutti i compilatori mainstream correnti), si hanno due scelte realistiche: utilizzare i puntatori raw o avvolgere il puntatore in un Pimpl personalizzato. Userei il Pimpl se lo sforzo è giustificabile. Non è un codice tremendamente complicato, ma devi fare attenzione a inoltrare tutte le operazioni necessarie:

class BaseClassIf {
public:
  virtual ~BaseClassIf() {}
  virtual void someOperation() = 0;
  virtual BaseClassIf* copy() = 0; // { return new T(*this); }
}

class BaseClass {
  BaseClassIf* impl;
  void assertInvariant() { if (!impl) throw ...; }
public:
  BaseClass(BaseClassIf* impl) : impl(impl) { assertInvariant(); }

  ~BaseClass() { if (impl) delete impl; impl = 0; }

  BaseClass(const BaseClass& o) : impl(o.impl->copy()) { assertInvariant(); }

  void someOperation() { impl->someOperation(); }

private:
  BaseClass& operator=(const BaseClass&);
  // Cannot be reasonably overloaded as a value-based copy,
  // e.g. "*impl = *rhs.impl" since the exact types are unknown.
  // Cannot be overloaded as a reference copy,
  // e.g. "impl = rhs.impl" since that violates pointer ownership.
};

Si noti che l'interfaccia deve fornire disposizioni per accedere al costruttore della copia, poiché il tipo esatto è sconosciuto dal wrapper BaseClass . Questa è una tecnica utile occasionalmente (ad esempio "cancellazione di tipo" per nascondere i parametri del modello), ma qui è solo un fastidioso fallout derivante dall'uso del polimorfismo. Si noti inoltre che non è possibile definire un operatore di assegnazione copia per l'adattatore, a meno che non si includa un metodo comune virtual BaseClassIf::operator=(const BaseClassIf&) nell'interfaccia, ma la maggior parte delle gerarchie di oggetti non può eseguire una copia utile dalla loro base comune.

    
risposta data 15.12.2015 - 21:06
fonte
5

Alla tua prima domanda: "è un buon stile lasciare che un metodo di restituzione di fabbrica sia un contenitore?":

IMHO non c'è niente di speciale con un tipo come std::vector<AbstractBaseClass*> - è, almeno in linea di principio, un tipo definito dall'utente come qualsiasi altro tipo. Potresti creare un alias typedef come

 typedef std::vector<AbstractBaseClass*> MyAbstractBaseClassContainer;

se questo ti dà una migliore astrazione, o la avvolgi in una nuova classe. Quest'ultimo ti dà l'opportunità di rendere privato il costruttore, impedendo così la creazione di oggetti di quel tipo con mezzi diversi rispetto a un metodo di produzione di membri di una classe statica. Ma questo non è necessariamente necessario per una fabbrica "buona" - se std::vector<AbstractBaseClass*> si adatta alle tue esigenze, vai avanti!

Se preferisci some_smart_ptr<AbstractBaseClass> a AbstractBaseClass* , specialmente nel contesto di vector è una domanda completamente indipendente dalla prima. Poiché c'erano già buone risposte a questa domanda, ad esempio qui o qui , o la risposta di @ amon, non sto cercando di rispondere a questo, dubito che la mia risposta sarebbe migliorata.

    
risposta data 15.12.2015 - 21:11
fonte