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 (besitzt 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 Move-Semantik – eine leistungsstarke Optimierung, die unnötiges Kopieren vermeidet.
Eine rvalue-Referenz, deklariert mit &&, bindet spezifisch an temporäre Objekte. Dies ermöglicht es Ihnen, Ressourcen von Objekten zu „stehlen“, die ohnehin bald verschwinden:
#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 Sie bereit sind, die Ressourcen des Objekts aufzugeben. Das eigentliche Verschieben findet im Move-Konstruktor oder im Move-Zuweisungsoperator statt.
Move-Semantiken verbessern die Performance drastisch, wenn mit ressourcenintensiven Objekten wie Containern oder Strings gearbeitet wird. Anstatt Megabytes an Daten tief zu kopieren, übertragen Sie einfach den Besitz des Pointers – eine Operation in konstanter Zeit, 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 Integern verwaltet.Ihre Klasse sollte private Member für den Daten-Pointer (
int*), die Größe (size_t) und einen Namen (std::string) haben, um zu verfolgen, welcher Buffer welcher ist während der Operationen.Deklarieren Sie Folgendes:
- Einen Konstruktor, der einen
std::stringNamen und einesize_tGröße entgegennimmt, das Array allokiert 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-Buffer den Namen der Quelle übernimmt) - Einen Destruktor, der
[name] destroyedausgibt (oderempty destroyed, wenn der Buffer verschoben wurde) - Eine
getSize()-Methode, die die aktuelle Größe zurückgibt - Eine
getName()-Methode, die den Namen des Buffers zurückgibt
Denken Sie daran, Ihren Move-Konstruktor als
noexceptzu markieren und das Quellobjekt in einem gültigen leeren Zustand zu hinterlassen (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 Pointer nicht null ist. Binden Sie<iostream>für die Ausgabe ein.main.cpp: Lesen Sie zwei Eingaben ein:- Einen Namen für Ihren Buffer (String)
- Eine Größe für Ihren Buffer (Integer)
Erstellen Sie einen
DataBuffermit dem angegebenen Namen und der Größe. Erstellen Sie dann einen zweiten Buffer, indem Sie den ersten mitstd::move()verschieben. Geben Sie nach dem Verschieben den Zustand beider Buffer 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 allokierten Speicher überträgt, ohne Daten zu kopieren. Der ursprüngliche Buffer wird in einem leeren, aber gültigen Zustand hinterlassen, und wenn beide Buffer am Ende des Programms zerstört werden, löscht nur derjenige, der noch Eigentümer des Speichers ist, diesen tatsächlich.
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: Erstellen Sie einen DataBuffer mit dem angegebenen Namen und der Größe
// TODO: Erstellen Sie einen zweiten Puffer, indem Sie den ersten mit std::move() verschieben
// TODO: Geben Sie den Zustand beider Puffer aus:
// Original: [name] Größe=[size]
// Neu: [name] Größe=[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