Menu
Coddy logo textTech

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 und kurz davor, zerstört zu werden). Das Verständnis dieses Unterschieds ermöglicht Move-Semantik – eine leistungsstarke Optimierung, die unnötiges Kopieren vermeidet.

Eine rvalue-Referenz, deklariert mit &&, bindet sich speziell an temporäre Objekte. Dadurch kannst du Ressourcen von Objekten „stehlen“, die ohnehin gleich 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 tatsächlich nichts – sie wandelt lediglich einen Lvalue in eine Rvalue-Referenz um und signalisiert damit, dass du bereit bist, die Ressourcen des Objekts aufzugeben. Das tatsächliche Verschieben erfolgt im Move-Konstruktor oder Move-Zuweisungsoperator.

Move-Semantik verbessert die Leistung bei der Arbeit mit ressourcenintensiven Objekten wie Containern oder Zeichenketten erheblich. Anstatt Megabytes an Daten tief zu kopieren, überträgst du einfach den Besitz des Zeigers – eine Operation mit konstanter Laufzeit unabhängig von der Größe.

challenge icon

Aufgabe

Einfach

Erstellen wir eine ressourcenverwaltende DataBuffer-Klasse, die Move-Semantik in der Praxis demonstriert. Du wirst sehen, wie das Verschieben von Ressourcen statt ihrer Kopie die Effizienz beim Übertragen des Besitzes dynamisch reservierten Speichers erheblich verbessern kann.

Du organisierst deinen Code auf drei Dateien:

  • DataBuffer.h: Definiere deine DataBuffer-Klasse, die ein dynamisch reserviertes Array von Ganzzahlen verwaltet.

    Deine Klasse sollte private Member für den Datenzeiger (int*), die Größe (size_t) und einen Namen (std::string) besitzen, damit während der Operationen nachvollzogen werden kann, welcher Puffer welcher ist.

    Deklariere Folgendes:

    • Einen Konstruktor, der einen std::string-Namen und eine size_t-Größe entgegennimmt, das Array reserviert und Folgendes ausgibt: [name] constructed with size [size]
    • Einen Move-Konstruktor, der eine Rvalue-Referenz entgegennimmt, die Ressourcen übernimmt und Folgendes ausgibt: [name] moved from [source_name] (wobei der Zielpuffer den Namen der Quelle übernimmt)
    • Einen Destruktor, der [name] destroyed ausgibt (oder empty 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

    Denke daran, deinen Move-Konstruktor mit noexcept zu kennzeichnen und das Quellobjekt in einem gültigen leeren Zustand zu belassen (nullptr, Größe 0, Name "empty").

  • DataBuffer.cpp: Implementiere alle in deinem Header deklarierten Methoden. Wenn der Destruktor ausgeführt wird, darf er die Daten nur löschen, wenn der Zeiger nicht null ist. Binde <iostream> für die Ausgabe ein.
  • main.cpp: Lies zwei Eingaben ein:
    1. Einen Namen für deinen Puffer (String)
    2. Eine Größe für deinen Puffer (Ganzzahl)

    Erstelle einen DataBuffer mit dem angegebenen Namen und der angegebenen Größe. Erstelle anschließend einen zweiten Puffer, indem du den ersten mit std::move() verschiebst. Gib nach dem Verschieben den Zustand beider Puffer aus:

    • Original: [name] size=[size]
    • New: [name] size=[size]

    Binde <utility> für std::move ein.

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 destroyed

Mit 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 destroyed

Beachte, wie der Move-Konstruktor den Besitz des reservierten Speichers überträgt, ohne Daten zu kopieren. Der ursprüngliche Puffer bleibt 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.

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;
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung

Übe selbstständig: Online-C++-Compiler