=========================== Das Vererben von Ressourcen [TOC] =========================== Angenommen, wir verlagern das Erzeugen eines _IntegerSequence_-Objekts in eine Funktion und geben die fertige Sequenz über den Return-Wert zurück. Dann erscheint die Umkopier-Aktion unvermeidlich. Folgendes Beispiel demonstriert dies, indem _IntegerSequence_ signalisiert, welche der Methoden aufgerufen wird: :import: session03/step03/IntegerSequence.hpp [fold] :import: session03/step03/test_is.cpp ---- SHELL (path=session03/step03,hostname=theon) -------- g++ -Wall -o test_is test_is.cpp ./test_is ---------------------------------------------------------- Wie zu sehen ist, wird zuerst _iseq_ in _main_ mit dem _default constructor_ aufgebaut, dann wird die Funktion aufgerufen, bei der ein lokales _iseq_ angelegt und befüllt wird. Nach der Rückkehr wird dann mit dem Zuweisungs-Operator der Inhalt umkopiert und danach der Rückgabewert abgebaut. Copy elision ============ Bevor wir fragen, wie sich das vermeiden lässt, lohnt es sich, folgenden Fall zu betrachten: ---- SHELL (path=session03/step03,hostname=theon) -------- diff -U 2 test_is.cpp test_is2.cpp g++ -Wall -o test_is2 test_is2.cpp ./test_is2 ---------------------------------------------------------- Hier sehen wir, dass nur eine einzige Sequenz erzeugt und abgebaut wird. Das Umkopieren entfällt. Dieses Phänomen nennt sich in C++ _copy elision_, d.h. in besonderen Umständen darf der Übersetzer die Sache vereinfachen. Im zweiten Fall ist dies möglich, weil das zu _main_ lokale _iseq_-Objekt mit dem Return-Wert konstruiert wird. Wenn eine Funktion ein nicht-triviales Objekt zurückgibt, muss vom Aufrufer die hierfür benötigte Speicherfläche auf dem Stack reserviert werden, denn das Objekt wird ja noch temporär benötigt, wenn der Aufruf der Funktion abgeschlossen wird. Implementierungstechnisch kann man sich das so vorstellen, dass der Aufrufer der Funktion implizit einen weiteren Parameter mit der Adresse des Return-Werts übergibt. Wenn beim Aufrufer ein Objekt mit dem Return-Wert konstruiert wird, dann kann der Übersetzer sogleich die Adresse des neuen Objekts übergeben, womit der Kopieraufwand eingespart werden kann. Und wenn bei der Umsetzung der Funktion der Übersetzer sieht, dass nur eine einzige lokale Variable für den Return-Wert verwendet wird, dann für die lokale Variable von vornherein die Speicherfläche für den Return-Wert genommen werden. Letzteres wird mit NRVO bezeichnet (_named return value optimization_). Wenn beides zusammenkommt, entfällt das Kopieren vollständig (_zero copy overhead_). Das gehört zu den wenigen Fällen, bei der die Anwendung einer Optimierung sich bei den möglichen Seiteneffekten beobachten lässt (wie hier beispielsweise durch die Debug-Ausgaben). Der C++-Standard lässt dies in eine Reihe benannter Fälle ausdrücklich zu. Da der Übersetzer die Freiheit hat, diese Optimierung anzuwenden oder darauf zu verzichten, kann das Programm sich bei verschiedenen Übersetzern unterschiedlich verhalten. So verzichtet beispielsweise der mit `CC` aufzurufende Oracle-Studio-Übersetzer auf diese Optimierung: ---- SHELL (path=session03/step03,hostname=theon) -------- CC -std=c++11 -o test_is2 test_is2.cpp ./test_is2 ---------------------------------------------------------- Das Verschieben von Ressourcen ============================== Kehren wir zum ersten Fall zurück, bei dem _copy elision_ nicht zur Anwendung kommt, da das Zielobjekt bereits existiert. Hier haben wir ein temporäres Objekt auf der rechten Seite der Zuweisung: ---- CODE (type=cpp) ---------------------------------------------------------- IntegerSequence iseq; iseq = gen_sequence(); ------------------------------------------------------------------------------- Da das Objekt auf der rechten Seite der Zuweisung unmittelbar danach abgebaut wird, stellt sich die Frage, ob wir das nicht anders handhaben können. D.h. können wir "sterbende" Objekte als solche erkennen und in diesem Fall das Kopieren vermeiden? Beginnend mit C++11 wurden sogenannte _rvalue references_ eingeführt, d.h. Referenzen auf ein temporäres Objekt auf der rechten Seite. Sie ähneln syntaktisch den sogenannten _lvalue references_. Während letztere mit einem einfachen `&` gekennzeichnet werden, ist beim _declarator_ für eine _rvalue reference_ `&&` anzugeben. Der Handel sieht so aus: Wenn wir eine _rvalue reference_ für ein dem Tode geweihtes Objekt erhalten, dürfen wir es nach Herzenslust "ausplündern", müssen es aber in einem aufgeräumten Zustand zurücklassen, so dass es noch korrekt abgebaut werden kann. So könnte ein Zuweisungs-Operator aussehen, der eine _rvalue reference_ akzeptiert: ---- CODE (type=cpp) ---------------------------------------------------------- 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; } ------------------------------------------------------------------------------- Zu beachten ist, dass die _rvalue reference_ natürlich nicht mit `const` ausgezeichnet wird, da wir selbstverständlich darauf schreibend zugreifen möchten. Der einfachste Ansatz beim verschiebenden Zuweisungs-Operator besteht in dem Austausch aller Komponenten. Der alte Inhalt unseres Objekts landet dann bei "dem Tode geweihten" Objekt, das unmittelbar danach abgebaut werden wird. Die Funktion `std::swap` steht über `#include ` zur Verfügung und erhält zwei _lvalue references_ des gleichen Typs und tauscht diese gegeneinander aus. Diese Funktion ist für alle elementaren Typen und zahlreiche Typen der Standard-Bibliothek vordefiniert. Sie kann ergänzend für eigene Typen definiert werden. So sieht dann ein Testlauf aus: ---- SHELL (path=session03/step04,hostname=theon) -------- g++ -o test_is test_is.cpp ./test_is ---------------------------------------------------------- Aufgabe ======= Ergänzen Sie die Vorlage um einen Konstruktor, der ähnlich wie der Zuweisungs-Operator eine _rvalue reference_ akzeptiert und die Inhalte dann übernimmt (_move constructor_). Ergänzen Sie das Testprogramm dahingehend, dass alle Varianten zum Zuge kommen, d.h. der klassische Kopierkonstruktor neben dem _move constructor_ und die klassische Zuweisung neben dem _move assignment_. Vorlage ======= :import: session03/step04/IntegerSequence.hpp [fold] :import: session03/step04/test_is.cpp [fold] :navigate: up -> doc:index back -> doc:session03/page02 next -> doc:session03/page04