Wann müssen wir Kopierkonstruktoren verwenden?

85

Ich weiß, dass der C ++ - Compiler einen Kopierkonstruktor für eine Klasse erstellt. In welchem ​​Fall müssen wir einen benutzerdefinierten Kopierkonstruktor schreiben? Können Sie einige Beispiele nennen?

Penguru
quelle
1
Einer der Fälle, in denen Sie Ihren eigenen Kopierer schreiben müssen: Wenn Sie eine tiefe Kopie erstellen müssen. Beachten Sie außerdem, dass beim Erstellen eines Ctors kein Standard-Ctor für Sie erstellt wird (es sei denn, Sie verwenden das Standardschlüsselwort).
harschvchawla

Antworten:

74

Der vom Compiler generierte Kopierkonstruktor kopiert die Mitglieder. Manchmal reicht das nicht aus. Beispielsweise:

class Class {
public:
    Class( const char* str );
    ~Class();
private:
    char* stored;
};

Class::Class( const char* str )
{
    stored = new char[srtlen( str ) + 1 ];
    strcpy( stored, str );
}

Class::~Class()
{
    delete[] stored;
}

In diesem Fall storedwird der Puffer durch das kopieren des Mitglieds durch Mitglieder nicht dupliziert (nur der Zeiger wird kopiert), sodass die erste zu zerstörende Kopie, die den Puffer gemeinsam nutzt, delete[]erfolgreich aufgerufen wird und die zweite auf undefiniertes Verhalten stößt. Sie benötigen einen Kopierkonstruktor für tiefes Kopieren (und auch einen Zuweisungsoperator).

Class::Class( const Class& another )
{
    stored = new char[strlen(another.stored) + 1];
    strcpy( stored, another.stored );
}

void Class::operator = ( const Class& another )
{
    char* temp = new char[strlen(another.stored) + 1];
    strcpy( temp, another.stored);
    delete[] stored;
    stored = temp;
}
scharfer Zahn
quelle
10
Es wird keine bitweise, sondern eine mitgliedsbezogene Kopie ausgeführt, die insbesondere den Kopiercode für Mitglieder vom Klassentyp aufruft.
Georg Fritzsche
7
Schreiben Sie den Assingment-Operator nicht so. Es ist keine Ausnahme sicher. (Wenn der Neue eine Ausnahme auslöst, bleibt das Objekt in einem undefinierten Zustand, wobei der Speicher auf einen freigegebenen Teil des Speichers zeigt (geben Sie den Speicher NUR frei, nachdem alle Operationen, die ausgelöst werden können, erfolgreich abgeschlossen wurden)). Eine einfache Lösung ist die Verwendung des Copy-Swap-Idiums.
Martin York
@ Sharptooth 3. Zeile von unten haben Sie delete stored[];und ich glaube, es sollte seindelete [] stored;
Peter Ajtai
4
Ich weiß, dass es nur ein Beispiel ist, aber Sie sollten darauf hinweisen, dass die bessere Lösung die Verwendung ist std::string. Die allgemeine Idee ist, dass nur Dienstprogrammklassen, die Ressourcen verwalten, die Big Three überladen müssen und dass alle anderen Klassen nur diese Dienstprogrammklassen verwenden sollten, sodass keine der Big Three definiert werden muss.
GManNickG
2
@ Martin: Ich wollte sicherstellen, dass es in Stein gemeißelt ist. : P
GManNickG
45

Ich bin ein bisschen verärgert, dass die Regel der Rule of Fivenicht zitiert wurde.

Diese Regel ist sehr einfach:

Die Fünferregel :
Wenn Sie einen der Destruktoren, Kopierkonstruktoren, Kopierzuweisungsoperatoren, Verschiebungskonstruktoren oder Verschiebungszuweisungsoperatoren schreiben, müssen Sie wahrscheinlich die anderen vier schreiben.

Es gibt jedoch eine allgemeinere Richtlinie, die Sie befolgen sollten, die sich aus der Notwendigkeit ergibt, ausnahmesicheren Code zu schreiben:

Jede Ressource sollte von einem dedizierten Objekt verwaltet werden

Hier ist @sharptoothder Code immer noch (meistens) in Ordnung, aber wenn er seiner Klasse ein zweites Attribut hinzufügen würde, wäre dies nicht der Fall. Betrachten Sie die folgende Klasse:

class Erroneous
{
public:
  Erroneous();
  // ... others
private:
  Foo* mFoo;
  Bar* mBar;
};

Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}

Was passiert bei new BarWürfen? Wie löscht man das Objekt, auf das gezeigt wird mFoo? Es gibt Lösungen (Funktionsebene try / catch ...), sie skalieren einfach nicht.

Der richtige Weg, um mit der Situation umzugehen, besteht darin, anstelle von Rohzeigern geeignete Klassen zu verwenden.

class Righteous
{
public:
private:
  std::unique_ptr<Foo> mFoo;
  std::unique_ptr<Bar> mBar;
};

Mit der gleichen Konstruktorimplementierung (oder tatsächlich mit make_unique) habe ich jetzt die Ausnahmesicherheit kostenlos !!! Ist es nicht aufregend? Und das Beste ist, ich muss mir keine Sorgen mehr um einen richtigen Zerstörer machen! Ich muss meine eigenen schreiben Copy Constructorund Assignment Operatorzwar, weil unique_ptrdiese Operationen nicht definiert sind ... aber das spielt hier keine Rolle;)

Und deshalb hat sharptoothdie Klasse noch einmal besucht:

class Class
{
public:
  Class(char const* str): mData(str) {}
private:
  std::string mData;
};

Ich weiß nichts über dich, aber ich finde meine einfacher;)

Matthieu M.
quelle
Für C ++ 11 - die Fünferregel, die der Dreierregel den Verschiebungskonstruktor und den Verschiebungszuweisungsoperator hinzufügt.
Robert Andrzejuk
1
@Robb: Beachten Sie, dass tatsächlich, wie im letzten Beispiel demonstriert, Sie in der Regel für die darauf abzielen sollte Rule Of Zero . Nur spezialisierte (generische) technische Klassen sollten sich um den Umgang mit einer Ressource kümmern , alle anderen Klassen sollten diese intelligenten Zeiger / Container verwenden und sich keine Sorgen machen.
Matthieu M.
@MatthieuM. Einverstanden :-) Ich habe die Regel der Fünf erwähnt, da diese Antwort vor C ++ 11 liegt und mit "Big Three" beginnt, aber es sollte erwähnt werden, dass jetzt die "Big Five" relevant sind. Ich möchte diese Antwort nicht ablehnen, da sie im gestellten Kontext korrekt ist.
Robert Andrzejuk
@Robb: Guter Punkt, ich habe die Antwort aktualisiert, um die Regel der Fünf anstelle der Großen Drei zu erwähnen. Hoffentlich sind die meisten Leute inzwischen zu C ++ 11-fähigen Compilern übergegangen (und ich bedaure diejenigen, die es noch nicht getan haben).
Matthieu M.
31

Ich kann mich an meine Praxis erinnern und an die folgenden Fälle denken, in denen es darum geht, den Kopierkonstruktor explizit zu deklarieren / zu definieren. Ich habe die Fälle in zwei Kategorien eingeteilt

  • Korrektheit / Semantik - Wenn Sie keinen benutzerdefinierten Kopierkonstruktor angeben, können Programme, die diesen Typ verwenden, möglicherweise nicht kompiliert werden oder funktionieren möglicherweise nicht ordnungsgemäß.
  • Optimierung - Eine gute Alternative zum vom Compiler generierten Kopierkonstruktor ermöglicht es, das Programm schneller zu machen.


Korrektheit / Semantik

Ich füge in diesen Abschnitt die Fälle ein, in denen das Deklarieren / Definieren des Kopierkonstruktors für den korrekten Betrieb der Programme unter Verwendung dieses Typs erforderlich ist.

Nachdem Sie diesen Abschnitt gelesen haben, lernen Sie einige Fallstricke kennen, die es dem Compiler ermöglichen, den Kopierkonstruktor selbst zu generieren. Daher ist , wie seand in seiner bemerkte Antwort , ist es immer sicher Kopierbarkeit für eine neue Klasse auszuschalten und absichtlich es später ermöglichen , wenn es wirklich erforderlich ist .

So machen Sie eine Klasse in C ++ 03 nicht kopierbar

Deklarieren Sie einen privaten Kopierkonstruktor und stellen Sie keine Implementierung dafür bereit (sodass der Build in der Verknüpfungsphase fehlschlägt, selbst wenn die Objekte dieses Typs im eigenen Bereich der Klasse oder von ihren Freunden kopiert werden).

So machen Sie eine Klasse in C ++ 11 oder neuer nicht kopierbar

Deklarieren Sie den Kopierkonstruktor mit =deleteam Ende.


Shallow vs Deep Copy

Dies ist der am besten verstandene Fall und tatsächlich der einzige, der in den anderen Antworten erwähnt wird. Shaprtooth hat es ziemlich gut abgedeckt . Ich möchte nur hinzufügen, dass das gründliche Kopieren von Ressourcen, die ausschließlich dem Objekt gehören sollten, auf alle Arten von Ressourcen angewendet werden kann, von denen dynamisch zugewiesener Speicher nur eine Art ist. Bei Bedarf kann auch ein gründliches Kopieren eines Objekts erforderlich sein

  • Kopieren temporärer Dateien auf die Festplatte
  • Öffnen einer separaten Netzwerkverbindung
  • Erstellen eines separaten Arbeitsthreads
  • Zuweisen eines separaten OpenGL-Framebuffers
  • etc

Selbstregistrierende Objekte

Stellen Sie sich eine Klasse vor, in der alle Objekte - egal wie sie konstruiert wurden - irgendwie registriert sein MÜSSEN. Einige Beispiele:

  • Das einfachste Beispiel: Beibehalten der Gesamtzahl der aktuell vorhandenen Objekte. Bei der Objektregistrierung wird lediglich der statische Zähler erhöht.

  • Ein komplexeres Beispiel ist eine Singleton-Registrierung, in der Verweise auf alle vorhandenen Objekte dieses Typs gespeichert werden (damit Benachrichtigungen an alle gesendet werden können).

  • Referenzgezählte Smart-Zeiger können in dieser Kategorie nur als Sonderfall betrachtet werden: Der neue Zeiger "registriert" sich selbst bei der gemeinsam genutzten Ressource und nicht in einer globalen Registrierung.

Eine solche Selbstregistrierungsoperation muss von JEDEM Konstruktor des Typs ausgeführt werden, und der Kopierkonstruktor ist keine Ausnahme.


Objekte mit internen Querverweisen

Einige Objekte haben möglicherweise eine nicht triviale interne Struktur mit direkten Querverweisen zwischen ihren verschiedenen Unterobjekten (tatsächlich reicht nur ein solcher interner Querverweis aus, um diesen Fall auszulösen). Der Compiler bereitgestellten Copykonstruktor die internen brechen intra-Objekt Verbände, um sie zu konvertieren zwischen Objekten Verbände.

Ein Beispiel:

struct MarriedMan;
struct MarriedWoman;

struct MarriedMan {
    // ...
    MarriedWoman* wife;   // association
};

struct MarriedWoman {
    // ...
    MarriedMan* husband;  // association
};

struct MarriedCouple {
    MarriedWoman wife;    // aggregation
    MarriedMan   husband; // aggregation

    MarriedCouple() {
        wife.husband = &husband;
        husband.wife = &wife;
    }
};

MarriedCouple couple1; // couple1.wife and couple1.husband are spouses

MarriedCouple couple2(couple1);
// Are couple2.wife and couple2.husband indeed spouses?
// Why does couple2.wife say that she is married to couple1.husband?
// Why does couple2.husband say that he is married to couple1.wife?

Es dürfen nur Objekte kopiert werden, die bestimmte Kriterien erfüllen

Es kann Klassen geben, in denen Objekte in einem bestimmten Zustand (z. B. Standard-Konstruktionszustand) sicher kopiert und ansonsten nicht sicher kopiert werden können. Wenn wir das Kopieren von sicher zu kopierenden Objekten zulassen möchten, benötigen wir - wenn wir defensiv programmieren - eine Laufzeitprüfung im benutzerdefinierten Kopierkonstruktor.


Nicht kopierbare Unterobjekte

Manchmal aggregiert eine Klasse, die kopierbar sein sollte, nicht kopierbare Unterobjekte. Normalerweise geschieht dies für Objekte mit nicht beobachtbarem Zustand (dieser Fall wird im Abschnitt "Optimierung" weiter unten ausführlicher erläutert). Der Compiler hilft lediglich, diesen Fall zu erkennen.


Quasi kopierbare Unterobjekte

Eine Klasse, die kopierbar sein sollte, kann ein Unterobjekt eines quasi kopierbaren Typs aggregieren. Ein quasi kopierbarer Typ stellt keinen Kopierkonstruktor im engeren Sinne bereit, verfügt jedoch über einen anderen Konstruktor, mit dem eine konzeptionelle Kopie des Objekts erstellt werden kann. Der Grund dafür, einen Typ quasi kopierbar zu machen, liegt darin, dass keine vollständige Übereinstimmung über die Kopiersemantik des Typs besteht.

Wenn wir beispielsweise den Fall der Selbstregistrierung von Objekten erneut betrachten, können wir argumentieren, dass es Situationen geben kann, in denen ein Objekt nur dann beim globalen Objektmanager registriert werden muss, wenn es sich um ein vollständiges eigenständiges Objekt handelt. Wenn es sich um ein Unterobjekt eines anderen Objekts handelt, liegt die Verantwortung für dessen Verwaltung bei dem enthaltenen Objekt.

Oder es muss sowohl flaches als auch tiefes Kopieren unterstützt werden (keines davon ist die Standardeinstellung).

Die endgültige Entscheidung bleibt dann den Benutzern dieses Typs überlassen. Beim Kopieren von Objekten müssen sie explizit (durch zusätzliche Argumente) die beabsichtigte Kopiermethode angeben.

Im Falle eines nicht defensiven Programmieransatzes ist es auch möglich, dass sowohl ein regulärer Kopierkonstruktor als auch ein Quasi-Kopierkonstruktor vorhanden sind. Dies kann gerechtfertigt sein, wenn in den allermeisten Fällen eine einzige Kopiermethode angewendet werden sollte, während in seltenen, aber gut verstandenen Situationen alternative Kopiermethoden verwendet werden sollten. Dann beschwert sich der Compiler nicht darüber, dass er den Kopierkonstruktor nicht implizit definieren kann. Es liegt in der alleinigen Verantwortung des Benutzers, sich zu merken und zu prüfen, ob ein Unterobjekt dieses Typs über einen Quasi-Kopierkonstruktor kopiert werden soll.


Kopieren Sie keinen Status, der stark mit der Identität des Objekts verbunden ist

In seltenen Fällen kann eine Teilmenge des beobachtbaren Zustands des Objekts einen untrennbaren Teil der Identität des Objekts darstellen (oder als solcher betrachtet werden) und sollte nicht auf andere Objekte übertragbar sein (obwohl dies etwas kontrovers sein kann).

Beispiele:

  • Die UID des Objekts (aber diese gehört auch zum Fall "Selbstregistrierung" von oben, da die ID in einem Akt der Selbstregistrierung erhalten werden muss).

  • Verlauf des Objekts (z. B. des Rückgängig / Wiederherstellen-Stapels) für den Fall, dass das neue Objekt nicht den Verlauf des Quellobjekts erben darf, sondern mit einem einzelnen Verlaufselement " Kopiert um <ZEIT> von <OTHER_OBJECT_ID> " beginnt .

In solchen Fällen muss der Kopierkonstruktor das Kopieren der entsprechenden Unterobjekte überspringen.


Erzwingen der korrekten Signatur des Kopierkonstruktors

Die Signatur des vom Compiler bereitgestellten Kopierkonstruktors hängt davon ab, welche Kopierkonstruktoren für die Unterobjekte verfügbar sind. Wenn mindestens ein Unterobjekt keinen echten Kopierkonstruktor hat (das Quellobjekt als konstante Referenz verwendet), sondern einen mutierenden Kopierkonstruktor (das Quellobjekt als nicht konstante Referenz verwendet), hat der Compiler keine Wahl aber implizit einen mutierenden Kopierkonstruktor deklarieren und dann definieren.

Was ist nun, wenn der "mutierende" Kopierkonstruktor vom Typ des Unterobjekts das Quellobjekt nicht tatsächlich mutiert (und einfach von einem Programmierer geschrieben wurde, der das constSchlüsselwort nicht kennt )? Wenn wir diesen Code nicht durch Hinzufügen des fehlenden Codes reparieren können const, besteht die andere Möglichkeit darin, unseren eigenen benutzerdefinierten Kopierkonstruktor mit einer korrekten Signatur zu deklarieren und die Sünde zu begehen, sich an a zu wenden const_cast.


Copy-on-Write (COW)

Ein COW-Container, der direkte Verweise auf seine internen Daten preisgegeben hat, MUSS zum Zeitpunkt der Erstellung tief kopiert werden, da er sich sonst möglicherweise als Referenzzählhandle verhält.

Obwohl COW eine Optimierungstechnik ist, ist diese Logik im Kopierkonstruktor entscheidend für die korrekte Implementierung. Deshalb habe ich diesen Fall hier und nicht im Abschnitt "Optimierung" platziert, wo wir als nächstes vorgehen.



Optimierung

In den folgenden Fällen möchten / müssen Sie möglicherweise Ihren eigenen Kopierkonstruktor aus Optimierungsgründen definieren:


Strukturoptimierung beim Kopieren

Stellen Sie sich einen Container vor, der Elemententfernungsvorgänge unterstützt, dies jedoch möglicherweise, indem Sie das entfernte Element einfach als gelöscht markieren und seinen Steckplatz später wiederverwenden. Wenn eine Kopie eines solchen Containers erstellt wird, kann es sinnvoll sein, die überlebenden Daten zu komprimieren, anstatt die "gelöschten" Slots unverändert beizubehalten.


Überspringen Sie das Kopieren des nicht beobachtbaren Zustands

Ein Objekt kann Daten enthalten, die nicht Teil seines beobachtbaren Zustands sind. In der Regel handelt es sich dabei um zwischengespeicherte / gespeicherte Daten, die während der Lebensdauer des Objekts gesammelt wurden, um bestimmte langsame Abfragevorgänge zu beschleunigen, die vom Objekt ausgeführt werden. Das Kopieren dieser Daten kann sicher übersprungen werden, da sie neu berechnet werden, wenn (und wenn!) Die entsprechenden Vorgänge ausgeführt werden. Das Kopieren dieser Daten kann ungerechtfertigt sein, da es schnell ungültig werden kann, wenn der beobachtbare Zustand des Objekts (von dem die zwischengespeicherten Daten abgeleitet werden) durch Mutationsoperationen geändert wird (und wenn wir das Objekt nicht ändern wollen, warum erstellen wir eine Tiefe dann kopieren?)

Diese Optimierung ist nur gerechtfertigt, wenn die Hilfsdaten im Vergleich zu den Daten, die den beobachtbaren Zustand darstellen, groß sind.


Deaktivieren Sie das implizite Kopieren

Mit C ++ kann das implizite Kopieren deaktiviert werden, indem der Kopierkonstruktor deklariert wird explicit. Dann können Objekte dieser Klasse nicht an Funktionen übergeben und / oder von Funktionen nach Wert zurückgegeben werden. Dieser Trick kann für einen Typ verwendet werden, der leicht zu sein scheint, aber in der Tat sehr teuer zu kopieren ist (obwohl es eine bessere Wahl sein könnte, ihn quasi kopierbar zu machen).

In C ++ 03 muss für das Deklarieren eines Kopierkonstruktors auch dieser definiert werden (natürlich, wenn Sie ihn verwenden möchten). Wenn Sie sich für einen solchen Kopierkonstruktor nur aus dem besprochenen Grund heraus entschieden haben, mussten Sie denselben Code schreiben, den der Compiler automatisch für Sie generieren würde.

C ++ 11 und neuere Standards ermöglichen das Deklarieren spezieller Elementfunktionen (Standard- und Kopierkonstruktoren, Kopierzuweisungsoperator und Destruktor) mit einer expliziten Anforderung zur Verwendung der Standardimplementierung (beenden Sie die Deklaration einfach mit =default).



TODOs

Diese Antwort kann wie folgt verbessert werden:

  • Fügen Sie weiteren Beispielcode hinzu
  • Veranschaulichen Sie den Fall "Objekte mit internen Querverweisen"
  • Fügen Sie einige Links hinzu
Leon
quelle
6

Wenn Sie eine Klasse haben, die dynamisch Inhalt zugewiesen hat. Zum Beispiel speichern Sie den Titel eines Buches als Zeichen * und setzen den Titel auf neu. Die Kopie funktioniert nicht.

Sie müssten einen Kopierkonstruktor schreiben, der dies tut title = new char[length+1]und dann strcpy(title, titleIn). Der Kopierkonstruktor würde nur eine "flache" Kopie erstellen.

Peter Ajtai
quelle
2

Der Kopierkonstruktor wird aufgerufen, wenn ein Objekt entweder als Wert übergeben, als Wert zurückgegeben oder explizit kopiert wird. Wenn kein Kopierkonstruktor vorhanden ist, erstellt c ++ einen Standardkopierkonstruktor, der eine flache Kopie erstellt. Wenn das Objekt keine Zeiger auf dynamisch zugewiesenen Speicher hat, reicht eine flache Kopie aus.

Josh
quelle
0

Es ist oft eine gute Idee, copy ctor und operator = zu deaktivieren, es sei denn, die Klasse benötigt es speziell. Dies kann Ineffizienzen wie das Übergeben eines Args als Wert verhindern, wenn eine Referenz beabsichtigt ist. Auch die vom Compiler generierten Methoden können ungültig sein.

Seand
quelle
-1

Betrachten wir das folgende Code-Snippet:

class base{
    int a, *p;
public:
    base(){
        p = new int;
    }
    void SetData(int, int);
    void ShowData();
    base(const base& old_ref){
        //No coding present.
    }
};
void base :: ShowData(){
    cout<<this->a<<" "<<*(this->p)<<endl;
}
void base :: SetData(int a, int b){
    this->a = a;
    *(this->p) = b;
}
int main(void)
{
    base b1;
    b1.SetData(2, 3);
    b1.ShowData();
    base b2 = b1; //!! Copy constructor called.
    b2.ShowData();
    return 0;
}

Output: 
2 3 //b1.ShowData();
1996774332 1205913761 //b2.ShowData();

b2.ShowData();Gibt eine Junk-Ausgabe aus, da ein benutzerdefinierter Kopierkonstruktor erstellt wurde, für den kein Code zum expliziten Kopieren von Daten geschrieben wurde. Der Compiler erstellt also nicht dasselbe.

Ich dachte nur daran, dieses Wissen mit Ihnen allen zu teilen, obwohl die meisten von Ihnen es bereits wissen.

Prost ... Viel Spaß beim Codieren !!!

Pankaj Kumar Thapa
quelle