5

In C++ quando le classi contengono dati allocati dinamicamente è generalmente ragionevole definire il costruttore di copie, operator = e destructor. Ma l'attività di questi metodi speciali si sovrappone. Più specificatamente operator = di solito prima fa qualche distruzione e poi fa coping simile a quello in copy constructor.Evitare di ripetere lo stesso codice in copy constructor e operator =

La mia domanda è come scrivere questo nel modo migliore senza ripetere le stesse linee di codice e senza la necessità che il processore svolga il lavoro non necessario (come la copia non necessaria).

Di solito finisco con due metodi di aiuto. Uno per la costruzione e uno per la distruzione. Il primo è chiamato da copy constructor e operator =. Il secondo è usato da destructor e operator =.

Ecco il codice di esempio:

template <class T> 
    class MyClass 
    { 
     private: 
     // Data members 
     int count; 
     T* data; // Some of them are dynamicly allocated 
     void construct(const MyClass& myClass) 
     { 
      // Code which does deep copy 
      this->count = myClass.count; 
      data = new T[count]; 
      try 
      { 
       for (int i = 0; i < count; i++) 
        data[i] = myClass.data[i]; 
      } 
      catch (...) 
      { 
       delete[] data; 
       throw; 
      } 
     } 
     void destruct() 
     { 
      // Dealocate all dynamicly allocated data members 
      delete[] data; 
     } 
     public: MyClass(int count) : count(count) 
     { 
      data = new T[count]; 
     } 
     MyClass(const MyClass& myClass) 
     { 
      construct(myClass); 
     } 
     MyClass& operator = (const MyClass& myClass) 
     { 
      if (this != &myClass) 
      { 
       destruct(); 
       construct(myClass); 
      } 
      return *this; 
     } 
     ~MyClass() 
     { 
      destruct(); 
     } 
    }; 

Questo è anche corretto? Ed è una buona abitudine dividere il codice in questo modo?

+0

+1 perché la domanda ha contribuito a sensibilizzare. Sembra qualcosa che avrei scritto prima di leggere le risposte. – ToolmakerSteve

+0

Hm, raramente ho duplicato il codice in entrambi, dal momento che entrambi fanno cose completamente diverse: uno inizializza, uno assegna ... – PlasmaHH

+0

è la natura della "copia profonda" del suo design di classe che porta alla duplicazione. – ToolmakerSteve

risposta

0

Implementare l'assegnazione copiando prima il lato destro e poi scambiando quello. In questo modo ottieni anche un'eccezione di sicurezza, che il tuo codice sopra non fornisce. Si potrebbe finire con un contenitore danneggiato quando construct() fallisce dopo che destruct() ha avuto successo altrimenti, poiché il puntatore del membro fa riferimento ad alcuni dati deallocati e alla distruzione che sarà deallocato di nuovo, causando un comportamento indefinito.

foo& 
foo::operator=(foo const& rhs) 
{ 
    using std::swap; 
    foo tmp(rhs); 
    swap(*this, tmp); 
    return *this; 
} 
+0

Se il costrutto fallisce, perché è meglio finire con il vecchio (e non più destinato) contenuto, anziché con un contenitore vuoto? IMHO sarebbe più pulito avere un risultato di copia fallito in un contenitore vuoto. In particolare, un contenitore vuoto è facile da rilevare nel codice successivo; un contenitore con contenuti IT NON DEVE PIÙ AVERE È più difficile da rilevare in seguito. – ToolmakerSteve

+0

Supponendo che si desidera terminare il programma in caso di tale errore, in entrambi i casi funziona bene, ma è comunque necessario impostare 'data = nullptr' (che il codice originale non riesce a fare). – riv

+0

@ToolmakerSteve Dipende da ciò che vuoi. L'idioma swap garantisce la piena integrità transazionale; o riesci o non cambia nulla. Nella maggior parte dei casi, l'integrità transazionale completa non è necessaria per i singoli oggetti; tutto ciò che devi garantire è che l'oggetto può essere distrutto in caso di fallimento. (Cercare di garantire uno stato effettivo diverso da quello immutato probabilmente non è molto utile.) –

0

non vedo alcun problema inerente con che, fino a quando hai la certezza di non dichiarare costruire o distruggere virtuale.

Potresti essere interessato al capitolo 2 in Effective C++ (Scott Meyers), che è completamente dedicato ai costruttori, agli operatori di copia e ai distruttori.

Per le eccezioni, che il codice non gestisce come dovrebbe, considerare gli articoli 10 & 11 in C++ più efficace (Scott Meyers).

+1

Tranne che non è eccezionalmente sicuro. Se il 'new' in' construct' lancia (o il copying getta), allora l'oggetto viene lasciato in uno stato incoerente, in cui distruggerlo causerà un comportamento indefinito. –

+0

@JamesKanze Ovviamente hai ragione, ma la domanda riguarda una tecnica per evitare la duplicazione del codice, e questa tecnica non ha problemi inerenti, credo. –

+0

Non ha problemi inerenti, tranne che non funziona. La sicurezza delle eccezioni non è una funzione opzionale; è essenziale se il programma deve essere corretto. –

5

Un commento iniziale: il operator= fa non partenza da destructing, ma costruendo. In caso contrario, lascerà l'oggetto in uno stato non valido se la costruzione termina tramite un'eccezione . Il tuo codice non è corretto a causa di questo. (Si noti che la necessità di testare per l'assegnazione di auto è di solito un segno che l'operatore di assegnazione non è corretto.)

La soluzione classica per la manipolazione di questo è il linguaggio di swap: si aggiunge una funzione membro swap:

void MyClass:swap(MyClass& other) 
{ 
    std::swap(count, other.count); 
    std::swap(data, other.data); 
} 

che è garantito di non buttare. (Qui, semplicemente scambia un int e un puntatore, nessuno dei quali può buttare.) Poi si implementare l'operatore di assegnamento come:

MyClass& MyClass<T>::operator=(MyClass const& other) 
{ 
    MyClass tmp(other); 
    swap(tmp); 
    return *this; 
} 

Questo è semplice e diretto, ma qualsiasi soluzione in cui tutto le operazioni che potrebbero fallire sono finite prima di iniziare cambiando i dati è accettabile.Per un caso semplice come il tuo codice di , ad esempio:

MyClass& MyClass<T>::operator=(MyClass const& other) 
{ 
    T* newData = cloneData(other.data, other.count); 
    delete data; 
    count = other.count; 
    data = newData; 
    return *this; 
} 

(dove cloneData è una funzione membro che fa la maggior parte di ciò che il vostro construct fa, ma restituisce il puntatore, e non modificare nulla in this).

EDIT:

non direttamente correlate alla tua domanda iniziale, ma in generale, in questi casi, si fa non vogliono fare un new T[count] in cloneData (o construct, o qualsiasi altra cosa). Ciò costruisce tutto il del T con il costruttore predefinito e quindi li assegna. Il modo idiomatico di fare questo è qualcosa di simile:

T* 
MyClass<T>::cloneData(T const* other, int count) 
{ 
    // ATTENTION! the type is a lie, at least for the moment! 
    T* results = static_cast<T*>(operator new(count * sizeof(T))); 
    int i = 0; 
    try { 
     while (i != count) { 
      new (results + i) T(other[i]); 
      ++ i; 
     } 
    } catch (...) { 
     while (i != 0) { 
      -- i; 
      results[i].~T(); 
     } 
     throw; 
    } 
    return results; 
} 

Molto spesso, questo sarà fatto attraverso un gestore (privato) separato classe:

// Inside MyClass, private: 
struct Data 
{ 
    T* data; 
    int count; 
    Data(int count) 
     : data(static_cast<T*>(operator new(count * sizeof(T))) 
     , count(0) 
    { 
    } 
    ~Data() 
    { 
     while (count != 0) { 
      -- count; 
      (data + count)->~T(); 
     } 
    } 
    void swap(Data& other) 
    { 
     std::swap(data, other.data); 
     std::swap(count, other.count); 
    } 
}; 
Data data; 

// Copy constructor 
MyClass(MyClass const& other) 
    : data(other.data.count) 
{ 
    while (data.count != other.data.count) { 
     new (data.data + data.count) T(other.date[data.count]); 
     ++ data.count; 
    } 
} 

(e, naturalmente, l'idioma di swap per incarico). Questo consente coppie di conteggio/dati multipli senza alcun rischio di perdere l'eccezione di sicurezza .

+0

+1, grazie a @James, ora capisco meglio questo argomento. – ToolmakerSteve

+0

Questo è qualcosa di rivoluzionario che vale più di un + - "Si noti che la necessità di testare l'autoassegnazione è di solito un segno che l'operatore di assegnazione non è corretto." – SChepurin

+0

@James Kanze: un caso (in cui un collega si è imbattuto) è quando l'operatore di assegnazione deve eseguire una memcpy su una delle sue risorse. In tal caso l'autoassegnazione diventa una necessità, no? – ForeverLearning