5

In C++, wenn Klassen dynamisch zugeordnete Daten enthalten, ist es normalerweise sinnvoll, explizit den Kopierkonstruktor operator = und destructor zu definieren. Aber die Aktivität dieser speziellen Methoden überlappt sich. Genauer gesagt, operator = normalerweise zuerst etwas Zerstörung und dann macht es Coping ähnlich dem im Copy-Konstruktor.Vermeiden Sie, den gleichen Code im Kopierkonstruktor zu wiederholen und operator =

Meine Frage ist, wie man dies am besten schreibt, ohne die gleichen Codezeilen zu wiederholen und ohne dass der Prozessor unnötige Arbeit verrichten muss (wie unnötiges Kopieren).

Ich habe normalerweise zwei helfende Methoden. Eine für den Bau und eine für die Zerstörung. Die erste wird sowohl von copy constructor als auch operator = aufgerufen. Die zweite wird von destructor und operator = verwendet. Hier

ist der Beispielcode:

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

Ist das auch richtig? Und ist es eine gute Angewohnheit, den Code auf diese Weise zu teilen?

+0

+1, weil die Frage half, mein Bewusstsein zu erhöhen. Es sieht so aus, als hätte ich etwas geschrieben, bevor ich die Antworten gelesen habe. – ToolmakerSteve

+0

Hm, ich habe Code in beiden selten dupliziert, da sie beide ganz verschiedene Dinge tun: man initialisiert, man ordnet ... – PlasmaHH

+0

es ist die "tiefe Kopie" Natur seines Klassenentwurfs, der zur Vervielfältigung führt. – ToolmakerSteve

Antwort

0

Implementieren Sie die Zuweisung, indem Sie zuerst die rechte Seite kopieren und dann mit ihr tauschen. Auf diese Weise erhalten Sie auch Ausnahmesicherheit, die Ihr Code oben nicht zur Verfügung stellt. Sie könnten mit einem beschädigten Container enden, wenn construct() fehlschlägt, nachdem destruct() erfolgreich ausgeführt wurde, weil der Memberzeiger auf einige nicht zugeordnete Daten verweist und auf die Destruction, die wieder freigegeben wird, was zu undefiniertem Verhalten führt.

+0

Wenn Konstrukt fehlschlägt, warum ist es besser, mit dem alten zu enden (und nicht mehr beabsichtigt)) Inhalt statt mit einem leeren Behälter? IMHO wäre es sauberer, wenn eine fehlerhafte Kopie in einem leeren Container resultiert. Insbesondere ist ein leerer Container im späteren Code leicht zu erkennen; Ein Behälter, der Inhalt hat, der nicht länger sein sollte, ist später schwerer zu erkennen. – ToolmakerSteve

+0

Vorausgesetzt, Sie möchten das Programm im Falle eines solchen Fehlers beenden, funktioniert jeder Weg, aber Sie müssen immer noch festlegen, "data = nullptr" (was der ursprüngliche Code nicht tun). – riv

+0

@ToolmakerSteve Es hängt davon ab, was Sie wollen. Das Swap-Idiom gewährleistet volle Transaktionsintegrität; Sie haben Erfolg, oder nichts ändert sich. In den meisten Fällen ist die vollständige Transaktionsintegrität für einzelne Objekte nicht erforderlich. Alles, was Sie garantieren müssen, ist, dass das Objekt im Fehlerfall zerstört werden kann. (Es ist wahrscheinlich nicht sehr nützlich, einen anderen Zustand als den unveränderten zu versuchen.) –

0

Ich sehe kein inhärentes Problem damit, solange Sie sicherstellen, nicht zu konstruieren oder zu destruieren virtuell.

Sie könnten Interesse an Kapitel 2 in Effektiv C++ (Scott Meyers), die vollständig auf Konstruktoren, Kopieroperatoren und Destruktoren gewidmet ist.

Für Ausnahmen, die Ihr Code nicht so behandelt, wie es sollte, beachten Sie Artikel 10 & 11 in effektiver C++ (Scott Meyers).

+1

Außer dass es nicht ausnahmesicher ist. Wenn das "neue" in "Konstrukt" wirft (oder das Kopieren wirft), dann verbleibt das Objekt in einem inkohärenten Zustand, in dem es zu undefiniertem Verhalten führen wird. –

+0

@JamesKanze Natürlich hast du Recht, aber die Frage ist eine Technik, die Code-Duplikation vermeidet, und diese Technik hat keine inhärenten Probleme, denke ich. –

+0

Es hat keine inhärenten Probleme, _except_, dass es nicht funktioniert. Ausnahmesicherheit ist keine optionale Funktion; Es ist wichtig, dass das Programm korrekt ist. –

5

Ein erster Kommentar: Die operator= tut nicht Start von zerstörenden, aber durch den Bau. Andernfalls wird das Objekt in einem ungültigen Zustand belassen, wenn die Konstruktion über eine Ausnahme beendet wird. Ihr Code ist deshalb falsch. (Beachten Sie, dass die Notwendigkeit für die Selbstzuordnung zu testen, ist in der Regel ein Zeichen dafür, dass der Zuweisungsoperator ist nicht korrekt.)

Die klassische Lösung für diese Handhabung ist das Swap-Idiom: Sie eine Memberfunktion Swap hinzufügen:

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

die garantiert nicht zu werfen ist. (Hier ist es tauscht nur einen int und einen Zeiger, kann keiner von denen werfen.) Dann Sie den Zuweisungsoperator implementieren, wie:

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

Das ist einfach und geradlinig, aber jede Lösung, in der alle Vorgänge, die fehlschlagen können, sind beendet, bevor Sie beginnen Ändern der Daten ist akzeptabel.Für einen einfachen Fall wie Ihr Code, zum Beispiel:

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

(wo cloneData ist Mitglied Funktion, die tut das meiste, was Ihre construct nicht, aber die Zeiger zurückgibt, und nichts in this ändern).

EDIT:

nicht direkt auf Ihre erste Frage, aber in der Regel, in solchen Fällen tun Sie nicht eine new T[count] in cloneData tun wollen (oder construct, oder was auch immer). Dies erstellt alle der T mit dem Standardkonstruktor und weist sie dann zu. Der idiomatische Weg, dies zu tun, ist so etwas wie:

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

oft meisten wird dieser Manager mit einem separaten (privat) erfolgen Klasse:

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

(und natürlich das Swap-Idiom für die Zuordnung). Dies ermöglicht mehrere Anzahl/Datenpaare ohne das Risiko, die Ausnahme Sicherheit zu verlieren.

+0

+1, danke @James, ich verstehe dieses Thema jetzt besser. – ToolmakerSteve

+0

Dies ist etwas revolutionär wert mehr als eins + - "Beachten Sie, dass die Notwendigkeit, für die Selbstzuweisung zu testen, normalerweise ein Zeichen dafür ist, dass der Zuweisungsoperator nicht korrekt ist." – SChepurin

+0

@James Kanze: Ein Fall (in den ein Mitarbeiter hineingeraten ist) ist, wenn Ihr Zuweisungsoperator ein Memcpy auf einer seiner Ressourcen machen muss. In diesem Fall wird Selbstaufgabe zu einer Notwendigkeit, nicht? – ForeverLearning