Domanda

Che cosa è un buon modello di classe / progetto esistente per il multi-fase di costruzione / l'inizializzazione di un oggetto in C ++?

ho una classe con alcuni membri di dati che dovrebbero essere inizializzati in diversi punti di flusso del programma, per cui la loro inizializzazione deve essere ritardata. Per esempio un argomento può essere letta da un file ed un altro dalla rete.

Attualmente sto usando boost :: opzionale per la costruzione ritardo dei membri di dati, ma mi preoccupa che optional è semanticamente differente che ritardare-costruito.

Quello che mi serve ricorda caratteristiche di boost :: bind e lambda applicazione funzione parziale, e l'utilizzo di queste librerie Probabilmente posso progettare costruzione in più fasi - ma io preferisco usare esistenti, classi testati. (O forse c'è un altro modello di costruzione in più fasi, che io non conosco).

È stato utile?

Soluzione

La questione chiave è se o non si dovrebbe distinguere gli oggetti completamente popolati da oggetti non completamente popolate a livello di tipo . Se si decide di non fare una distinzione, poi basta usare boost::optional o simili, come si sta facendo: questo lo rende facile ottenere la codifica in fretta. OTOH non è possibile ottenere il compilatore di far rispettare il requisito che una particolare funzione richiede un oggetto completamente popolata; è necessario eseguire in fase di esecuzione il controllo dei campi di volta in volta.

Tipi

Parametro del gruppo

Se fai distinguere gli oggetti completamente popolate dagli oggetti non completamente popolati a livello di tipo, è possibile rispettare il requisito che una funzione essere passato un oggetto completo. Per fare questo vorrei suggerire la creazione di un tipo corrispondente XParams per ogni tipo X rilevante. XParams ha membri boost::optional e funzioni setter per ogni parametro che può essere impostato dopo la costruzione iniziale. Poi si può forzare X di avere un solo costruttore (non-copy), che accetta un XParams come suo unico argomento e controlla che ogni parametro necessario è stata impostata all'interno di quell'oggetto XParams. (Non so se questo modello ha un nome? - nessuno come modificare questo per riempire noi in)

Tipi di "oggetto parziale"

Questo funziona meravigliosamente se non si ha realmente bisogno di do qualsiasi cosa con l'oggetto prima che sia completamente popolato (forse diverso da cose banali come ottenere i valori di campo di nuovo). Se si ha a trattare a volte un X incompleto popolato come un X "pieno", si può invece fare X derivare da un tipo XPartial, che contiene tutta la logica, oltre a protected metodi virtuali per l'esecuzione di test precondizione che mettono alla prova se tutti i campi necessari sono popolato. Poi, se X assicura che si può sempre e solo essere costruito in uno stato completamente popolato, è possibile ignorare quei metodi protetti con controlli banali che restituisce sempre true:

class XPartial {
    optional<string> name_;

public:
    void setName(string x) { name_.reset(x); }  // Can add getters and/or ctors
    string makeGreeting(string title) {
        if (checkMakeGreeting_()) {             // Is it safe?
            return string("Hello, ") + title + " " + *name_;
        } else {
            throw domain_error("ZOINKS");       // Or similar
        }
    }
    bool isComplete() const { return checkMakeGreeting_(); }  // All tests here

protected:
    virtual bool checkMakeGreeting_() const { return name_; }   // Populated?
};

class X : public XPartial {
    X();     // Forbid default-construction; or, you could supply a "full" ctor

public:
    explicit X(XPartial const& x) : XPartial(x) {  // Avoid implicit conversion
        if (!x.isComplete()) throw domain_error("ZOINKS");
    }

    X& operator=(XPartial const& x) {
        if (!x.isComplete()) throw domain_error("ZOINKS");
        return static_cast<X&>(XPartial::operator=(x));
    }

protected:
    virtual bool checkMakeGreeting_() { return true; }   // No checking needed!
};

Anche se potrebbe sembrare l'eredità qui è "al contrario", facendo in questo modo significa che un X può tranquillamente essere fornito da nessuna parte un XPartial& viene chiesto, quindi questo approccio obbedisce alla Liskov principio di sostituzione di . Ciò significa che una funzione può utilizzare un tipo di parametro di X& per indicare che deve essere un oggetto X completa, o per indicare XPartial& può gestire oggetti parzialmente popolate -. In questo caso possono essere passati sia un oggetto o un XPartial X intero

In origine avevo isComplete() come protected, ma ha trovato questo non ha funzionato dal momento ctor copia del X e assegnazione devono chiamare questa funzione sul loro argomento XPartial&, e non hanno accesso sufficiente. Riflettendoci, ha più senso per esporre pubblicamente questa funzionalità.

Altri suggerimenti

Devo mancare qualcosa qui - faccio questo genere di cose tutto il tempo. E 'molto comune avere oggetti che sono grandi e / o non necessari da una classe in tutte le circostanze. Così crearli dinamicamente!

struct Big {
    char a[1000000];
};

class A {
  public: 
    A() : big(0) {}
   ~A() { delete big; }

   void f() {
      makebig();
      big->a[42] = 66;
   }
  private:
    Big * big;
    void makebig() {
      if ( ! big ) {
         big = new Big;
      }
    }
};

Non vedo la necessità di qualcosa di più elaborato di quello, se non che makebig () dovrebbe probabilmente essere const (e forse in linea), e il Big puntatore dovrebbe probabilmente essere mutevole. E, naturalmente, una deve essere in grado di costruire Big, che può in altri casi significano la memorizzazione nella cache i parametri di costruzione della classe contenuto. Sarà inoltre necessario decidere su una politica di copiatura / assegnazione -. Probabilmente mi proibisco sia per questo tipo di classe

Non so di eventuali modelli per affrontare questo problema specifico. E 'una questione di progettazione difficile, e un po' unica per linguaggi come C ++. Un altro problema è che la risposta a questa domanda è strettamente legata al vostro individuo (o aziendale) stile di codifica.

Vorrei utilizzare i puntatori per questi membri, e quando hanno bisogno di essere costruito, li destinare allo stesso tempo. È possibile utilizzare auto_ptr per questi, e verificare contro NULL per vedere se vengono inizializzati. (Credo di puntatori sono un tipo built-in "optional" in C / C ++ / Java, ci sono altre lingue in cui NULL non è un puntatore valido).

Un problema come una questione di stile è che si può essere affidamento sui vostri costruttori di fare troppo lavoro. Quando sto codifica OO, ho i costruttori fanno appena sufficiente lavoro per ottenere l'oggetto in uno stato consistente. Per esempio, se ho una classe Image e voglio leggere da un file, avrei potuto fare questo:

image = new Image("unicorn.jpeg"); /* I'm not fond of this style */

o, avrei potuto fare questo:

image = new Image(); /* I like this better */
image->read("unicorn.jpeg");

Si può ottenere difficile ragionare su come un programma C ++ funziona se i costruttori hanno un sacco di codice in loro, soprattutto se si pone la domanda, "che cosa succede se un costruttore fallisce?" Questo è il vantaggio principale del movimento di codice fuori dei costruttori.

avrei altro da dire, ma non so che cosa stai cercando di fare con ritardato la costruzione.

Edit: mi sono ricordato che c'è un modo (un po 'perversa) di chiamare un costruttore su un oggetto in qualsiasi momento arbitrario. Ecco un esempio:

class Counter {
public:
    Counter(int &cref) : c(cref) { }
    void incr(int x) { c += x; }
private:
    int &c;
};

void dontTryThisAtHome() {
    int i = 0, j = 0;
    Counter c(i);       // Call constructor first time on c
    c.incr(5);          // now i = 5
    new(&c) Counter(j); // Call the constructor AGAIN on c
    c.incr(3);          // now j = 3
}

Si noti che facendo qualcosa di sconsiderato come questo potrebbe guadagnare il disprezzo dei tuoi compagni di programmatori, a meno che non hai solide ragioni per l'utilizzo di questa tecnica. Anche questo non ritarda il costruttore, solo ti permette di chiamare nuovamente in seguito.

Utilizzando boost.optional si presenta come una buona soluzione per alcuni casi di utilizzo. Non ho giocato molto con esso quindi non posso commentare molto. Una cosa che tenere a mente quando si tratta di tale funzionalità è se posso usare costruttori di overload al posto di default e di copia costruttori.

Quando ho bisogno di tale funzionalità vorrei solo usare un puntatore al tipo di campo necessaria in questo modo:

public:
  MyClass() : field_(0) { } // constructor, additional initializers and code omitted
  ~MyClass() {
    if (field_)
      delete field_; // free the constructed object only if initialized
  }
  ...
private:
  ...
  field_type* field_;

prossimo, invece di utilizzare il puntatore vorrei accedere al campo tramite il seguente metodo:

private:
  ...
  field_type& field() {
    if (!field_)
      field_ = new field_type(...);
    return field_;
  }
semantica

ho omesso const di accesso

Il modo più semplice che conosco è simile alla tecnica suggerita da Dietrich Epp, tranne che permette di ritardare veramente la costruzione di un oggetto fino a un momento della vostra scelta.

In sostanza: prenotare l'oggetto utilizzando malloc invece di nuovo (quindi bypassando il costruttore), quindi chiamare il nuovo operatore di overload quando si davvero vogliono costruire l'oggetto tramite nuova collocazione

.

Esempio:

Object *x = (Object *) malloc(sizeof(Object));
//Use the object member items here. Be careful: no constructors have been called!
//This means you can assign values to ints, structs, etc... but nested objects can wreak havoc!

//Now we want to call the constructor of the object
new(x) Object(params);

//However, you must remember to also manually call the destructor!
x.~Object();
free(x);

//Note: if you're the malloc and new calls in your development stack 
//store in the same heap, you can just call delete(x) instead of the 
//destructor followed by free, but the above is the  correct way of 
//doing it

Personalmente, l'unica volta che ho mai usato questa sintassi è stato quando ho dovuto usare un allocatore a base di C su misura per gli oggetti C ++. Come suggerisce Dietrich, si dovrebbe chiedersi se è davvero, davvero necessario ritardare la chiamata al costruttore. Il di base costruttore deve eseguire il minimo indispensabile per ottenere l'oggetto in uno stato utile, mentre altri costruttori di overload possono svolgere più lavoro, se necessario.

Non so se c'è un modello formale per questo. Nei luoghi in cui l'ho visto, l'abbiamo chiamato "pigro", "domanda" o "on demand".

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top