Copy & Swap Idiom
Content |
Wenn wir uns die letzte Fassung der IntegerSequence ansehen, dann fällt uns auf, dass der Kopierkonstruktor und der Zuweisungsoperator einander ähneln. Auch zwischen dem move constructor und dem move assignment gibt es Gemeinsamkeiten. In beiden mit rvalue-Referenzen arbeitenden Fällen sind alle Komponenten auszutauschen, wobei beim move constructor das „sterbende“ Objekt in den Zustand gebracht wird, den auch der default constructor liefern würde.
Deswegen gibt es für C++ einen geschickten Ansatz zur Vereinfachung entsprechender Klassen, bei der die unnötige Doppelarbeit vermieden wird. Der funktioniert dahingehend, dass wir
-
im copy constructor einmal definieren, wie kopiert wird und
-
in einer swap-Funktion einmal alle Komponenten zweier Objekte mit Hilfe der swap-Funktionen für die einzelnen Komponenten austauschen.
Wir benötigen dann folgendes:
-
Einen default constructor, der ein „leeres“ Objekt erzeugt.
-
Eine swap-Funktion, die keine Methode ist, aber innerhalb der Klasse als friend deklariert wird und somit uneingeschränkten Zugriff auch auf die privaten Komponenten hat. Diese Funktion erhält zwei Referenzen auf zwei Objekte und tauscht alle Komponenten untereinander aus. Die Funktion gibt keinen Wert zurück und erhält daher den Return-Typ void.
-
Einen copy constructor wie gehabt, der in der Lage ist, ein Objekt vollständig zu klonen.
-
Einen move constructor, der im Initialisierungsteil (hinter dem „:“ und vor der „{“) ein „leeres“ Objekt mit dem default constructor erzeugt und dann im compound statement (d.h. innerhalb von „{...}“) die oben definierte swap-Funktion verwendet, um die Inhalte des „sterbenden“ Objekts mit denen des eigenen, noch „leeren“ Objekts austauscht. Beachten Sie hierbei, dass this ein Zeiger auf das eigene Objekt ist und somit *this eine Referenz auf das eigene Objekt.
-
Und schließlich nur noch einen einzigen Zuweisungsoperator, der ein anderes Objekt als Werteparameter erhält und eine Referenz auf das eigene Objekt zurückliefert. Beim Aufruf gibt es zwei denkbare Fälle:
-
Es wird eine rvalue übergeben: Dann kann der lokale Werteparameter mit dem move constructor erzeugt werden und so die Inhalte übernehmen, ohne dass sie kopiert werden.
-
Es wird eine lvalue übergeben: Dann wird der lokale Werteparameter mit dem copy constructor erzeugt und die Inhalte geklont, was in diesem Fall unvermeidlich ist.
Somit wählt der Übersetzer in jedem Fall automatisiert die richtige Variante aus. Innerhalb der Funktion kann dann die swap-Funktion verwendet werden, um die Inhalte auszutauschen. Der lokale Werteparameter wird dann am Ende automatisch abgebaut.
-
Aufgabe
Vereinfachen Sie die letzte Lösung entsprechend dem copy and swap idiom in der oben beschriebenen Form und testen Sie Ihre Lösung wieder mit valgrind und dem bereits zuvor eingesetzten Testprogramm. Vergleichen Sie die Ausgaben der Testprogramme.
#ifndef INTEGER_SEQUENCE_HPP #define INTEGER_SEQUENCE_HPP #include <cassert> #include <cstdlib> #include <new> #include <iostream> class IntegerSequence { public: IntegerSequence() : data(nullptr), size(0), allocated(0) { std::cout << "IntegerSequence: default constructor" << std::endl; } IntegerSequence(const IntegerSequence& other) : data( other.data? static_cast<int*>(std::malloc(other.allocated * sizeof(int))) : nullptr ), size(other.size), allocated(other.allocated) { if (size > 0 && !data) { throw std::bad_alloc(); } for (std::size_t i = 0; i < size; ++i) { data[i] = other.data[i]; } std::cout << "IntegerSequence: copy constructor copying " << size << " elements" << std::endl; } IntegerSequence(IntegerSequence&& other) : data(other.data), size(other.size), allocated(other.allocated) { other.data = nullptr; other.size = 0; other.allocated = 0; std::cout << "IntegerSequence: move constructor moving " << size << " elements" << std::endl; } ~IntegerSequence() { std::cout << "IntegerSequence: destructor deleting " << size << " elements" << std::endl; std::free(data); } IntegerSequence& operator=(const IntegerSequence& other) { if (other.size > allocated) { int* newdata = static_cast<int*>(std::realloc(data, other.allocated * sizeof(int))); if (!newdata) { throw std::bad_alloc(); } data = newdata; allocated = other.allocated; } size = other.size; for (std::size_t i = 0; i < size; ++i) { data[i] = other.data[i]; } std::cout << "IntegerSequence: assignment operator copying " << size << " elements" << std::endl; return *this; } IntegerSequence& operator=(IntegerSequence&& other) { std::swap(data, other.data); std::swap(size, other.size); std::swap(allocated, other.allocated); std::cout << "IntegerSequence: move assignment taking " << size << " elements" << std::endl; return *this; } void add(int value) { if (size == allocated) { std::size_t newsize = allocated * 2 + 8; int* newdata = static_cast<int*>(std::realloc(data, newsize * sizeof(int))); if (!newdata) { throw std::bad_alloc(); } data = newdata; allocated = newsize; } data[size++] = value; } std::size_t length() const { return size; } int& operator()(std::size_t index) { assert(index < size); return data[index]; } const int& operator()(std::size_t index) const { assert(index < size); return data[index]; } private: int* data; std::size_t size; std::size_t allocated; }; #endif
#include <cstdlib> #include <iostream> #include "IntegerSequence.hpp" void print(const IntegerSequence& is) { for (std::size_t i = 0; i < is.length(); ++i) { std::cout << " " << is(i); } std::cout << std::endl; } IntegerSequence gen_sequence(IntegerSequence seq, int val) { for (int i = 1; i <= val; ++i) { seq.add(i); } return seq; } int main() { IntegerSequence iseq; // default constructor iseq.add(1); IntegerSequence iseq2{iseq}; // copy constructor iseq2.add(2); iseq = iseq2; // regular assignment operator std::cout << "iseq: "; print(iseq); IntegerSequence iseq3 = gen_sequence(iseq, 3); // move constructor std::cout << "iseq3: "; print(iseq3); IntegerSequence iseq4; iseq4 = gen_sequence(iseq, 3); // move assignment std::cout << "iseq4: "; print(iseq4); }