Move-Semantik & Rvalues
Teil des Abschnitts Objektorientierte Programmierung der C++-Journey von Coddy — Lektion 83 von 104.
In C++ ist jeder Ausdruck entweder ein lvalue (hat eine dauerhafte Identität, kann adressiert werden) oder ein rvalue (temporär, steht kurz vor der Zerstörung). Das Verständnis dieser Unterscheidung ermöglicht die Move-Semantik – eine leistungsstarke Optimierung, die unnötiges Kopieren vermeidet.
Eine R-Wert-Referenz, deklariert mit &&, bindet sich spezifisch an temporäre Objekte. Dies ermöglicht es Ihnen, Ressourcen von Objekten zu „stehlen“, die ohnehin bald verschwinden werden:
#include <iostream>
#include <utility>
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : data(new int[s]), size(s) {
std::cout << "Constructed\n";
}
// Move-Konstruktor - stiehlt Ressourcen
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Hinterlässt die Quelle in einem gültigen Zustand
other.size = 0;
std::cout << "Moved\n";
}
~Buffer() { delete[] data; }
};
int main() {
Buffer b1(1000);
Buffer b2(std::move(b1)); // Löst den Move-Konstruktor aus
}Die Funktion std::move verschiebt eigentlich gar nichts – sie wandelt lediglich einen lvalue in eine rvalue-Referenz um und signalisiert damit, dass man bereit ist, die Ressourcen des Objekts aufzugeben. Das eigentliche Verschieben findet im Move-Konstruktor oder im Move-Zuweisungsoperator statt.
Move-Semantiken verbessern die Performance drastisch, wenn man mit ressourcenintensiven Objekten wie Containern oder Strings arbeitet. Anstatt Megabytes an Daten tief zu kopieren, übertragen Sie einfach den Besitz des Pointers – eine Operation mit konstanter Laufzeit, unabhängig von der Größe.
Aufgabe
EinfachLassen Sie uns eine Ressourcen verwaltende DataBuffer-Klasse erstellen, die Move-Semantik in Aktion demonstriert. Sie werden sehen, wie das Verschieben von Ressourcen anstelle des Kopierens die Effizienz beim Übertragen des Eigentums an dynamisch zugewiesenem Speicher drastisch verbessern kann.
Sie werden Ihren Code über drei Dateien organisieren:
DataBuffer.h: Definieren Sie IhreDataBuffer-Klasse, die ein dynamisch zugewiesenes Array von Ganzzahlen verwaltet.Ihre Klasse sollte private Member für den Datenzeiger (
int*), die Größe (size_t) und einen Namen (std::string) haben, um zu verfolgen, welcher Puffer welcher ist während der Operationen.Deklarieren Sie Folgendes:
- Einen Konstruktor, der einen
std::stringNamen und einesize_tGröße entgegennimmt, das Array zuweist und ausgibt:[name] constructed with size [size] - Einen Move-Konstruktor, der eine Rvalue-Referenz entgegennimmt, die Ressourcen stiehlt und ausgibt:
[name] moved from [source_name](wobei der Ziel-Puffer den Namen der Quelle übernimmt) - Einen Destruktor, der
[name] destroyedausgibt (oderempty destroyed, wenn der Puffer verschoben wurde) - Eine
getSize()-Methode, die die aktuelle Größe zurückgibt - Eine
getName()-Methode, die den Namen des Puffers zurückgibt
Denken Sie daran, Ihren Move-Konstruktor als
noexceptzu markieren und das Quellobjekt in einem gültigen leeren Zustand zu belassen (nullptr, Größe 0, Name "empty").- Einen Konstruktor, der einen
DataBuffer.cpp: Implementieren Sie alle in Ihrem Header deklarierten Methoden. Wenn der Destruktor ausgeführt wird, löschen Sie die Daten nur, wenn der Zeiger nicht null ist. Binden Sie<iostream>für die Ausgabe ein.main.cpp: Lesen Sie zwei Eingaben ein:- Einen Namen für Ihren Puffer (String)
- Eine Größe für Ihren Puffer (Integer)
Erstellen Sie einen
DataBuffermit dem angegebenen Namen und der Größe. Erstellen Sie dann einen zweiten Puffer, indem Sie den ersten mitstd::move()verschieben. Geben Sie nach dem Verschieben den Zustand beider Puffer aus:Original: [name] size=[size]New: [name] size=[size]
Binden Sie
<utility>fürstd::moveein.
Zum Beispiel mit den Eingaben Alpha und 100:
Alpha constructed with size 100
Alpha moved from Alpha
Original: empty size=0
New: Alpha size=100
Alpha destroyed
empty destroyedMit den Eingaben Buffer und 50:
Buffer constructed with size 50
Buffer moved from Buffer
Original: empty size=0
New: Buffer size=50
Buffer destroyed
empty destroyedBeachten Sie, wie der Move-Konstruktor das Eigentum am zugewiesenen Speicher überträgt, ohne Daten zu kopieren. Der ursprüngliche Puffer verbleibt in einem leeren, aber gültigen Zustand, und wenn beide Puffer am Ende des Programms zerstört werden, löscht nur derjenige, der den Speicher noch besitzt, diesen tatsächlich.
Spickzettel
C++-Ausdrücke sind entweder lvalues (persistent, adressierbar) oder rvalues (temporär). Move-Semantiken optimieren die Performance, indem sie Ressourcen von temporären Objekten übertragen, anstatt sie zu kopieren.
Rvalue-Referenzen
Deklariert mit &&, binden rvalue-Referenzen an temporäre Objekte:
Buffer(Buffer&& other) // Rvalue-Referenz-ParameterMove-Konstruktor
Stiehlt Ressourcen von einem temporären Objekt. Markieren Sie ihn als noexcept und lassen Sie die Quelle in einem gültigen leeren Zustand zurück:
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // Quelle gültig lassen
other.size = 0;
}std::move
Castet einen lvalue in eine rvalue-Referenz und signalisiert die Bereitschaft, Ressourcen zu übertragen. Binden Sie <utility> ein:
Buffer b1(1000);
Buffer b2(std::move(b1)); // Löst den Move-Konstruktor ausstd::move verschiebt selbst nichts – die eigentliche Übertragung findet im Move-Konstruktor oder im Move-Zuweisungsoperator statt.
Vorteile
Move-Semantiken bieten eine Ressourcenübertragung in konstanter Zeit, unabhängig von der Objektgröße, und vermeiden so teure tiefe Kopien großer Datenstrukturen.
Probier es selbst
#include <iostream>
#include <string>
#include <utility>
#include "DataBuffer.h"
int main() {
std::string name;
int size;
std::cin >> name;
std::cin >> size;
// TODO: Erstelle einen DataBuffer mit dem angegebenen name und der size
// TODO: Erstelle einen zweiten Puffer, indem du den ersten mittels std::move() verschiebst
// TODO: Gib den Zustand beider Puffer aus:
// Original: [name] size=[size]
// Neu: [name] size=[size]
return 0;
}
Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.
Alle Lektionen in Objektorientierte Programmierung
1Grundlagen der OOP
Externe DateienC++ Build & KompilierungHeader-Dateien & QuelldateienNamensräume & GültigkeitsbereicheEinführung in OOP in C++Klassen vs. ObjekteDer 'this'-PointerMethoden (Elementfunktionen)Attribute (Datenelemente)Grundlagen zu Ctors & DtorsRückblick - Einfacher Taschenrechner4Klasseneigenschaften
Instanz- vs. statische MemberGetter und SetterConst-MemberfunktionenMutable-SchlüsselwortStatische Methoden und VariablenFriend-Funktionen & KlassenZusammenfassung - Bankkonto-Manager7Vererbung
Grundlagen der VererbungZugriffsstufen bei VererbungAufrufreihenfolge von Ctor & DtorÜberschreiben von MethodenVirtuelle Funktionen & VTableMehrfachvererbungVirtuelle VererbungRückblick - Mitarbeiter-Hierarchie2Speicherverwaltung
Stack vs. Heap SpeicherZeiger und ReferenzenDynamischer Speicher (new/delete)Smart Pointers in C++RAII in C++Rückblick – Dynamischer Array-Manager5Kapselung
Zugriffsspezifizierer in C++Zugriffsspezifizierer im DetailInformation HidingStruct vs. ClassVerschachtelte & innere KlassenRückblick - Studentenverwaltungssystem8Polymorphie
Kompilierzeit- vs. Laufzeit-PolymorphieFunktionsüberladungVirtuelle Funktionen – WiederholungRein virtuelle FunktionenAbstrakte KlassenInterface-Design in C++Dynamic Casting & RTTIZusammenfassung – Formen-Rechner3Konstruktoren & Destruktoren
StandardkonstruktorParametrisierter KonstruktorKopierkonstruktorVerschiebekonstruktorKonstruktor-InitialisierungslistenDelegierende KonstruktorenDestruktoren im DetailRule of Three / Five / ZeroZusammenfassung - String-Klasse6Operatorüberladung
Einführung in die OperatorüberladungArithmetische OperatorüberladungVergleichsoperatorüberladungStream-OperatorenZuweisungsoperatorüberladungÜberladen der [] und () OperatorenTypumwandlungsoperatorenRückblick - Matrix-Klasse9Templates
FunktionstemplatesKlassentemplatesTemplate-SpezialisierungVariadische TemplatesSFINAE & Type Traits GrundlagenRückblick - Generische Container12Moderne C++ Features
Move-Semantik & RvaluesPerfect ForwardingLambda-Ausdrücke im Detailstd::function & std::bindconstexpr und constevalStructured Bindingsoptional, variant, any