2013-03-26 11 views
8

L'attività di GotW#8 consiste nell'implementare una struttura di dati di stack generici eccezionalmente neutri in C++, supponendo che solo il distruttore dell'argomento del modello non getti. Il trucco consiste nel gestire potenzialmente le operazioni degli argomenti template di lancio (costruttore, costruttore di copie, assegnazione) in modo da lasciare lo stack in uno stato coerente se lanciano.Una tecnica di classe base per la gestione delle eccezioni

Nella soluzione, Herb Sutter dice

Per mantenere questa soluzione più semplice, ho deciso di non dimostrare la tecnica di classe di base per sicura rispetto alle eccezioni proprietà delle risorse.

Dopo un po 'googling, ho trovato this answer da Dave Abrahams risalente al 1997. Nella sua soluzione si gestisce l'allocazione e la cancellazione della memoria nella classe base, e implementa le operazioni di stack nella sottoclasse. In questo modo, assicura che la copia degli elementi nel costruttore di copie sia separata dall'allocazione della memoria, in modo che se la copia fallisce, viene chiamato il distruttore della classe base, a prescindere da cosa.

Per riferimento, ecco costruttore di copia di Dave con il mio commento ha aggiunto:

// v_ refers to the internal array storing the stack elements 
Stack(const Stack& rhs) 
     : StackBase<T>(rhs.Count()) // constructor allocates enough space 
             // destructor calls delete[] appropriately 
{ 
     while (Count() < rhs.Count()) 
      Push(rhs.v_[ Count() ]); // may throw 
} 

Se il costruttore di base ha successo, la memoria clean-up nel distruttore di base è garantito anche se il costruttore copia della sottoclasse getta.

Le mie domande sono:

  1. C'è qualche altro beneficio per l'approccio, ad eccezione di quanto descritto sopra?
  2. sono arrivato fino a questo costruttore di copia quando ho risolto il problema per conto mio:

    // v_ refers to the internal array storing the stack elements 
    // vsize_ is the amount of space allocated in v_ 
    // vused_ is the amount of space used so far in v_ 
    Stack (const Stack &rhs) : 
         vsize_ (0), vused_ (0), v_ (0) { 
        Stack temp (rhs.vused_); // constructor calls `new T[num_elements]` 
              // destructor calls `delete[] v_` 
        std::copy (rhs.v_, rhs.v_ + rhs.vused_, temp.v_); // may throw 
        swap (temp); 
    } 
    void swap (Stack &rhs) { 
        std::swap (v_, rhs.v_); 
        std::swap (vused_, rhs.vused_); 
        std::swap (vsize_, rhs.vsize_); 
    } 
    

    lo trovo un po 'ingombrante per avere una classe base rispetto a questo approccio. C'è qualche ragione per cui la tecnica della classe base dovrebbe essere preferita rispetto a questo approccio temp-copy-then-swap? Nota che sia Dave che io abbiamo già il membro swap() perché lo usiamo nel nostro operator=().

  3. La tecnica di Dave Abrahams non sembra essere molto nota (secondo Google). Ha un nome diverso, è una pratica standard, ho perso qualcosa?

Note:

  • assumere Dave Push() in un ciclo per essere equivalente a mio uso di std::copy
  • teniamoci puntatori intelligenti fuori la risposta, come il loro uso avrebbe tolto il punto di gestione memoria esplicitamente in questo esercizio

risposta

1

Comportamentalmente le due implementazioni sono le stesse. Entrambi impostano un oggetto di allocazione di memoria gestito che pulirà all'uscita dell'ambito se il costruttore fallisce. La copia su una variabile temporanea potrebbe essere più costosa ma, come indicato nei commenti, lo std::move probabilmente annullerebbe tali costi aggiuntivi. In risposta alle vostre domande specifiche:

  1. L'esempio di Abraham sposta l'allocazione heap più lontano dai dettagli effettivi dell'implementazione della classe.Nel codice, se si esegue una manipolazione della memoria più complicata prima/dopo la copia dell'array, potrebbe essere leggermente più difficile garantire una corretta gestione di tutte le entità. Altrimenti non vedo alcun dettaglio esplicito che non abbia già riguardato oltre lo stile per quanto riguarda il comportamento della prima implementazione.
  2. L'implementazione di Abraham estrae la pulizia in un'unica posizione. Se più classi usano StackBase<T>, allora ognuno può tranquillamente assumere che la propria memoria dinamica verrà ripulita se lancia un'eccezione. Nella tua implementazione dovresti riscrivere (almeno parti di) l'oggetto temporaneo e il codice di scambio per ottenere lo stesso risultato. In effetti, questa implementazione riduce il numero di linee per implementare più sottoclassi di StackBase. Tuttavia, l'implementazione evita l'ereditarietà multipla se si desidera un'ulteriore allocazione di memoria su altre classi base. Il tuo codice evita anche il gonfiamento del codice del template in termini di tempo/dimensioni, anche se in genere non ritengo che questo sia un grosso negativo nella maggior parte dei casi a prescindere. Probabilmente userò qualcosa di simile al tuo codice come predefinito a meno che non stia cercando di scrivere un codice di caso di uso molto generale.
  3. Non so se questo approccio ha un nome specifico - aggiornerò questo se ne trovo uno - ma l'ho visto usato in almeno un libro di programmazione C++ prima.
+0

La condivisione della base sembra essere l'unico argomento a favore dell'approccio di classe base. Gli darò un giorno o due per vedere se qualcun altro esce con un altro argomento. – Irfy

+0

L'approccio alla classe base vince quando il contenuto dell'oggetto è pesante e lo scambio con 'temp' è costoso. (Anche se il 'move' di C++ 11 potrebbe aiutare) Puoi aggiungere questo bit alla tua risposta, se lo desideri. – Irfy

Problemi correlati