CodeGym /Kurse /C++ SELF /std::unique_ptr — get() / reset() / release()

std::unique_ptr — get() / reset() / release()

C++ SELF
Level 43, Lektion 1
Verfügbar

1. Besitzgrenze

Bevor Sie auf die Knöpfe get(), reset() und release() drücken, lohnt es sich, eine halbe Sekunde innezuhalten und sich eine erwachsene Frage zu stellen: „Wer wird nach dieser Operation Besitzer der Ressource sein?“.

In C++ sehen Besitzfehler selten wie „eine schöne Fehlermeldung“ aus — meistens sind es Leaks, unerwartete Abstürze oder „scheint zu funktionieren, aber manchmal nicht“ (und das ist besonders gefährlich).

Stellen Sie sich vor, die Ressource ist der einzige Wohnungsschlüssel. unique_ptr ist die Person, die diesen Schlüssel aufbewahrt und sich verpflichtet, die Wohnung abzuschließen, wenn sie geht. Dann ist get() „den Schlüssel nur zum Anschauen geben, nicht übergeben“, reset() ist „den Schlüssel wegwerfen und die Wohnung schließen (oder die Wohnung wechseln)“, und release() ist „den Schlüssel dauerhaft an eine andere Person übergeben und nicht mehr verantwortlich sein“. Und gerade beim letzten Punkt machen Leute normalerweise dramatische Fehler.

Zeichnen wir ein einfaches Flussdiagramm zur Auswahl der Operation:

flowchart TD
    A["Ich möchte etwas mit unique_ptr tun"] --> B{"Muss der Besitzer gewechselt werden?"}
    B -->|Nein, nur Zugriff| C["get() (zusehen)"]
    B -->|Ja| D{"Wer wird nach der Operation Besitzer sein?"}
    D -->|Niemand: die Ressource soll verschwinden oder ersetzt werden| E["reset() (zerstören/ersetzen)"]
    D -->|Jemand anderes, aber nicht ein unique_ptr| F["release() (nach außen geben) + sofortiger Plan fuer den neuen Besitzer"]

Die Kernidee: get() ändert den Besitzer nicht, reset() zerstört (oder ersetzt) die Ressource des aktuellen Besitzers, release() macht den Besitzer zu… jemandem außerhalb, und das ist gefährlich.

2. get(): „zum Anschauen geben“

Wenn Sie zum ersten Mal p.get() sehen, könnte Ihre Hand dazu neigen, das als „nun, das ist ein roher Zeiger, also kann man damit alles machen“ zu interpretieren. Und hier lächelt C++ verschmitzt.

get() gibt tatsächlich T* zurück, aber in der Bedeutung ist das ein temporärer Beobachter: der Zugriff auf das Objekt wird erlaubt, die Verantwortung für die Freigabe verbleibt jedoch beim unique_ptr.

Ein typisches Szenario für get() — Sie haben eine Funktion, die historisch einen rohen Zeiger akzeptiert (oder es ist einfacher, „nullable“ so auszudrücken), aber sie soll das Objekt nicht löschen. Dann übergeben Sie p.get(), und alles ist gut — solange Sie nicht versuchen, aus diesem Zeiger einen zweiten Besitzer zu machen oder ihn „für später“ zu speichern.

Mini‑Beispiel: wir haben eine Applikations‑„Session“, die wir ausdrucken. Das Drucken soll die Session nicht besitzen — es liest nur die Daten.

#include <iostream>
#include <memory>
#include <string>

struct Session {
    std::string user;
};

void print_session(const Session* s) {
    if (!s) { std::cout << "no session\n"; return; } // no session
    std::cout << "user = " << s->user << '\n';       // user = Alice
}

Und jetzt die Verwendung von get():

#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    print_session(sess.get());
}

Beachten Sie: print_session nimmt ein const Session*, das heißt, es kann ein nullptr erhalten. Das ist ein guter Vertrag für „Zuschauer“. Und sess bleibt der Besitzer.

Ein wichtiges Anti‑Beispiel, das zwar kompiliert, aber logisch falsch ist:

#include <memory>

int main() {
    auto a = std::make_unique<int>(5);
    std::unique_ptr<int> b(a.get()); // DANGEROUS: zweiter Besitzer!
}

Warum das schlecht ist: sowohl a als auch b glauben, Besitzer desselben int zu sein. Wenn beide zerstört werden — gibt es eine doppelte Freigabe. Damit haben Sie den Sinn von unique_ptr selbst zunichte gemacht.

Eine weitere „stille“ Falle von get() — einen Zeiger länger zu speichern, als das Leben des Besitzers garantiert ist. Wenn Sie T* raw = p.get() genommen haben und dann irgendwo vorher p.reset() ausgeführt wurde, zeigt raw nun auf bereits freigegebenen Speicher. Der Zeiger wird nicht automatisch zu nullptr, weil er „dumm“ ist (er ist nur eine Adresse).

3. reset(): Ressource zerstören oder ersetzen

reset() ist eine Operation, die sich meistens wie „aufräumen“ anfühlt. Sie ist nötig, wenn der Besitzer entweder die Ressource loslassen (leer werden) oder die Ressource durch eine andere ersetzen muss.

Und das klingt einfach — aber halten Sie im Hinterkopf: reset() ändert nicht nur die Adresse innerhalb des unique_ptr, es löst außerdem die Zerstörung des alten Objekts aus (falls vorhanden).

Im menschlichen Sinn beantwortet reset() die Frage: „Ich als Besitzer möchte dieses Objekt nicht länger besitzen.“ Und dann wird das Objekt sofort zerstört, nicht „irgendwann später“. Deshalb ist reset() ein gutes Werkzeug, wenn Sie eine Ressource vorzeitig freigeben wollen (z. B. damit Speicher vor Ende der Funktion freigegeben wird).

Mini‑Beispiel: wir haben eine optionale Session. Beim Befehl „logout“ wollen wir die Session entfernen.

#include <iostream>
#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess.reset();                       // session geloescht, sess ist leer
    std::cout << std::boolalpha << !sess << '\n'; // true
}

Nun die Variante „Ressource ersetzen“. Stellen Sie sich vor, „login“ erzeugt die Session neu.

#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess = std::make_unique<Session>(Session{"Bob"}); // alte Session geloescht
}

Technisch steht hier nicht explizit reset(), aber vom Effekt her passiert dasselbe: die alte Ressource wird freigegeben, die neue angenommen. Man kann auch so schreiben:

#include <memory>
#include <string>

struct Session { std::string user; };

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice"});
    sess.reset(new Session{"Bob"}); // funktioniert, aber make_unique ist meist besser
}

Warum std::make_unique besser ist — das haben wir vorher besprochen: es macht die Absicht klarer und reduziert manuelle Verwaltungsstücke.

Wichtig ist der Effekt: nach reset() oder einem Austausch werden alle „Beobachter“ (rohe T*, die zuvor via get() erhalten wurden) potenziell gefährlich. Nicht weil reset() schlecht ist, sondern weil Sie das Objekt ersetzt/zerstört haben und die Beobachter davon nichts wissen.

4. release(): Besitz nach außen geben

release() ist die tückischste der drei Operationen. Sie sieht aus wie „fast wie get()“, macht aber in der Bedeutung das Gegenteil.

Wenn get() einen rohen Zeiger ohne Besitzübergabe liefert, dann gibt release() einen rohen Zeiger mit Besitzübergabe: unique_ptr hört auf, Besitzer zu sein und wird leer (nullptr).

Das heißt, nach release() liegt die Verantwortung für delete wieder bei Ihnen. Ja, bei genau dem „Sie“, der sich ursprünglich für unique_ptr entschieden hat, weil er nicht über delete nachdenken wollte.

Deshalb die einfache Regel: release() nur verwenden, wenn Sie genau wissen, wer der nächste Besitzer wird, und in der Regel — sofort.

Mini‑Beispiel zur Veranschaulichung:

#include <iostream>
#include <memory>

int main() {
    auto p = std::make_unique<int>(10);

    int* raw = p.release();                 // p ist leer
    std::cout << (p == nullptr) << '\n';    // 1

    delete raw; // jetzt muessen wir selbst loeschen (sonst Leak)
}

Im echten „modernen“ Code wollen Sie das nicht so machen. Ein sichereres Szenario ist, dass Sie release() nur benutzen, um sofort den Besitz an einen anderen Besitzer zu übergeben (z. B. an eine API, die T* akzeptiert und verspricht, „jetzt ich bin Besitzer“).

Zum Beispiel können wir eine Funktion schreiben, die „den Besitz annimmt und wieder in ein unique_ptr verpackt“, sodass an der Grenze zwischen „altem/neuem System“ rohe Zeiger nicht lange herumliegen:

#include <memory>

std::unique_ptr<int> adopt(int* p) {
    return std::unique_ptr<int>(p);
}

int main() {
    auto a = std::make_unique<int>(7);
    auto b = adopt(a.release()); // Besitz ist umgezogen, a ist leer
}

Die Idee ist richtig: der rohe Zeiger existiert sehr kurz und wird sofort wieder in einen RAII‑Besitzer gelegt.

Noch einmal: release() ist nicht „nur den Zeiger holen“. Es ist „den Besitz abgeben“.

Merkblatt: get() vs reset() vs release()

Wenn der Kopf müde ist, hilft eine Tabelle. Wichtig ist hier nicht „wie es aussieht“, sondern „was mit dem Besitzer passiert“ und „wer für die Löschung verantwortlich ist“.

Operation Was wird zurückgegeben Ändert es den Besitzer? Zustand des unique_ptr danach Wer löscht das Objekt
get()
T*
Nein Unverändert Immer noch unique_ptr
reset()
nichts Ja (Besitzer ändert/löscht die Ressource) Normalerweise
nullptr
(oder neue Ressource)
unique_ptr löscht die alte Ressource selbst
release()
T*
Ja (Besitz geht nach außen) Wird zu
nullptr
Derjenige, der das
T*
erhalten hat (manuell oder neuer Besitzer)

Diese Logik passt gut zur „Regel eines Besitzers“: get() fügt keinen Besitzer hinzu, reset() fügt keinen Besitzer hinzu (der Besitzer bleibt derselbe, nur die Ressource ändert sich), aber release() sagt wörtlich: „jetzt gibt es keinen Besitzer mehr innerhalb des unique_ptr“.

5. Praktisches Beispiel: ToDo‑Session und Besitzgrenzen

Damit das Thema nicht rein theoretisch bleibt, bauen wir unsere kleine Konsolen‑Übungsanwendung (ein einfaches ToDo/Notizen‑Programm) weiter. Wir bleiben bei prozeduralem Stil und machen einfache structs und Funktionen.

Beginnen wir mit den Daten: Aufgaben und Session.

#include <string>
#include <vector>

struct Task {
    std::string text;
    bool done{};
};

struct Session {
    std::string user;
    std::vector<Task> tasks;
};

Jetzt machen wir zwei „Zuschauer“-Funktionen: eine druckt die Session, die andere die Anzahl der Aufgaben. Sie nehmen const Session*, weil es die Session eventuell nicht gibt (z. B. der Benutzer ist nicht eingeloggt).

#include <iostream>

void print_user(const Session* s) {
    if (!s) { std::cout << "guest\n"; return; }     // guest
    std::cout << s->user << '\n';                   // Alice
}

void print_task_count(const Session* s) {
    if (!s) { std::cout << 0 << '\n'; return; }     // 0
    std::cout << s->tasks.size() << '\n';           // 2
}

Und jetzt im main erstellen wir ein unique_ptr<Session> und geben sicher „zum Anschauen“ mit get():

#include <memory>

int main() {
    std::unique_ptr<Session> sess; // leer: nicht eingeloggt

    print_user(sess.get());

    sess = std::make_unique<Session>(Session{"Alice", {{"Buy milk", false}}});
    print_user(sess.get());
}

Hier ist get() das perfekte Werkzeug: wir übergeben keinen Besitz, wir erlauben nur Lesezugriff.

Nun fügen wir ein „logout“ hinzu, wo es natürlich ist, reset() zu verwenden:

#include <iostream>
#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice", {}});
    sess.reset();                                 // session geloescht
    std::cout << std::boolalpha << !sess << '\n'; // true
}

Die Idee ist einfach: der Besitzer ist derselbe (sess), aber er ist jetzt leer und das Objekt wurde zerstört.

Und schließlich — die Situation, in der jemand „von außen“ den Besitz übernehmen möchte. Im modernen Design wäre es besser, unique_ptr direkt zu akzeptieren, aber nehmen wir an, wir haben eine hypothetische „Legacy‑API“:

#include <iostream>

void legacy_take_session(Session* s) {
    // Angenommen, das Legacy‑System verspricht: "ich loesche s selbst"
    std::cout << "legacy got session\n"; // legacy got session
    delete s;
}

Dann sieht die Grenze so aus:

#include <memory>

int main() {
    auto sess = std::make_unique<Session>(Session{"Alice", {}});
    legacy_take_session(sess.release()); // Besitz nach außen gegeben
    // sess ist jetzt leer, und das ist in Ordnung
}

Hier ist release() gerechtfertigt: Sie geben den Besitz wirklich nach außen, und der nächste Besitzer ist eindeutig definiert (die Legacy‑Funktion). Aber wenn die Legacy‑Funktion das Objekt nicht löschen sollte — entsteht ein Leak. Wenn sie es löscht und Sie später nochmals löschen — entsteht ein double‑free. Deshalb verlangt release() Disziplin und einen klaren Vertrag.

6. Typische Fehler

Fehler Nr. 1: aus get() einen zweiten Besitzer machen.
Das ist einer der unangenehmsten logischen Fehler, weil es „natürlich“ aussieht: Sie bekamen ein T* und wickeln es in einen neuen unique_ptr. Das Problem ist, dass der erste unique_ptr nicht verschwunden ist und weiterhin denkt, er sei Besitzer. Ergebnis: zwei Besitzer versuchen, dieselbe Ressource freizugeben. Die korrekte Denkweise ist: get() ist nur zum Anschauen, kein neuer Besitzer.

Fehler Nr. 2: rohen Zeiger nach reset() oder Ressourcentausch verwenden.
Das passiert oft versehentlich: Sie haben Task* t = ...get() gespeichert, dann wurde irgendwo höher im Code reset() (oder einfach eine neue unique_ptr zugewiesen), und der Zeiger wurde plötzlich dangling. unique_ptr ist hier nicht schuld: er hat das Objekt korrekt zerstört. Wir sind schuld, weil wir vergessen haben, dass ein Beobachter das Leben des Besitzers nicht verlängert.

Fehler Nr. 3: release() als „einfachen Zugriff auf T*“ ansehen.
release() bedeutet „ich besitze nicht mehr, holt es euch und seid verantwortlich“. Wenn Sie nach release() den Zeiger nicht weiter an einen neuen Besitzer übergeben und ihn nicht manuell löschen, ist ein Leak nahezu garantiert. Deshalb wird release() selten genutzt und meist „an der Grenze“ zu alten APIs, wo Besitz durch rohe Zeiger ausgedrückt wird.

Fehler Nr. 4: denken, dass man nach release() das unique_ptr noch dereferenzieren kann.
Nach release() ist das unique_ptr leer (nullptr). Es ist als Objekt gültig (man kann ihm neu zuweisen), aber das Dereferenzieren mit * oder das Zugreifen via -> ist ohne Prüfung nicht erlaubt. Das ist dieselbe Disziplin wie nach std::move: die Quelle ist meist leer und sollte so behandelt werden, bis das Gegenteil gezeigt ist.

Fehler Nr. 5: „wer besitzt“ und „wer benutzt“ in einer Funktion mischen ohne Vertrag.
Beispielsweise nimmt eine Funktion ein Session* und löscht manchmal innerhalb, und manchmal nicht. Außenstehende können dann nicht erkennen, wer für die Ressource verantwortlich ist. Das Ergebnis ist, dass ein Aufruf ein Leak verursacht, der nächste ein double‑free. Selbst wenn Sie mit rohen Zeigern arbeiten müssen, halten Sie eine klare Regel: entweder die Funktion besitzt immer und löscht immer, oder sie löscht nie (und das muss am Design/Namen/Dokumentation sichtbar sein). unique_ptr hilft genau dabei, solche Grenzen klarer zu machen.

1
Aufgabe
C++ SELF, Level 43, Lektion 1
Gesperrt
Sensorbeobachter
Sensorbeobachter
1
Aufgabe
C++ SELF, Level 43, Lektion 1
Gesperrt
Ein- und Ausgabe
Ein- und Ausgabe
1
Aufgabe
C++ SELF, Level 43, Lektion 1
Gesperrt
Lebende Tokens
Lebende Tokens
1
Aufgabe
C++ SELF, Level 43, Lektion 1
Gesperrt
Altes Modul
Altes Modul
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION