Zum Inhalt springen

std::unique_ptr: exklusiver Besitz

Lerne, wie std::unique_ptr exklusiven Besitz ohne jeden Overhead bietet, wie du make_unique einsetzt, eigene Deleter für C-Ressourcen schreibst und Factory-Funktionen idiomatisch gestaltest.

Was ist unique_ptr?

std::unique_ptr ist ein Smart Pointer, der ein Objekt exklusiv besitzt — es gibt genau einen unique_ptr, der auf ein bestimmtes Objekt zeigt. Wird der unique_ptr zerstört (verlässt er seinen Gültigkeitsbereich), löscht er das besessene Objekt automatisch.

Wesentliche Eigenschaften:
- Nicht kopierbar — Kopieren würde zwei Besitzer erzeugen und damit den exklusiven Besitz verletzen.
- Verschiebbar — der Besitz lässt sich per std::move übertragen.
- Kein Overhead — sizeof(unique_ptr) == sizeof(T*) (mit dem Standard-Deleter). Der erzeugte Code ist identisch mit der Verwendung eines rohen Zeigers samt manuellem delete.
- Das ist der Standard-Smart-Pointer. Greife zuerst zu unique_ptr; verwende shared_ptr nur, wenn du wirklich geteilten Besitz brauchst.

make_unique: der einzig richtige Weg

std::make_unique(args...) (C++14) ist der einzig richtige Weg, einen unique_ptr zu erzeugen. Es ist ausnahmesicher, knapp und kommt ohne new aus.

unique_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Connection {
    std::string host;
    int port;
    Connection(std::string h, int p) : host(std::move(h)), port(p) {
        std::cout << "Connected to " << host << ":" << port << '\n';
    }
    ~Connection() {
        std::cout << "Disconnected from " << host << '\n';
    }
    void query(const std::string& sql) {
        std::cout << "Executing: " << sql << '\n';
    }
};

int main() {
    // CORRECT: use make_unique
    auto conn = std::make_unique<Connection>("localhost", 5432);
    conn->query("SELECT * FROM users");

    // Access the raw pointer (non-owning)
    Connection* raw = conn.get();  // does NOT transfer ownership

    // Transfer ownership
    auto conn2 = std::move(conn);
    // conn is now nullptr
    if (!conn) std::cout << "conn is null after move\n";

    // conn2 destroyed here — destructor called automatically
}  // Output: "Disconnected from localhost"

Eigene Deleter für C-Ressourcen

Viele C-Bibliotheken liefern rohe Zeiger zurück, die mit einer bestimmten Funktion (und nicht mit delete) freigegeben werden müssen. unique_ptr unterstützt dafür eigene Deleter. Der Deleter wird Teil des Typs, sodass bei einem Funktionszeiger oder einem zustandslosen Lambda kein Overhead entsteht.

custom_deleter.cpp
#include <cstdio>
#include <memory>
#include <iostream>

// Custom deleter for FILE*
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::fclose(f);
            std::cout << "File closed\n";
        }
    }
};

// Type alias for clarity
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

// Factory function
UniqueFile open_file(const char* path, const char* mode) {
    FILE* f = std::fopen(path, mode);
    if (!f) throw std::runtime_error("Cannot open file");
    return UniqueFile(f);
}

int main() {
    try {
        auto file = open_file("/tmp/test.txt", "w");
        std::fputs("Hello, RAII!\n", file.get());
        // file automatically closed when scope exits
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }

    // Lambda deleter (less common, but useful for one-offs)
    auto deleter = [](int* p) { std::cout << "custom free\n"; delete p; };
    std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
}

unique_ptr mit Arrays

std::unique_ptr verwaltet ein dynamisch allokiertes Array und ruft automatisch delete[] auf. Es bietet operator[] statt operator* und operator->.

auto arr = std::make_unique<int[]>(100);  // 100 mit null initialisierte ints
arr[0] = 42;

Allerdings ist std::vector fast immer die bessere Wahl — er kennt seine Größe, unterstützt Iteration und ist genauso effizient. Verwende unique_ptr nur bei der Anbindung an C-APIs oder wenn du einen nicht vergrößerbaren, nicht kopierbaren Puffer brauchst.

Factory-Funktionen, die unique_ptr zurückgeben

Einen unique_ptr aus einer Factory-Funktion zurückzugeben ist das idiomatische Besitzmuster in C++. Der Aufrufer erhält exklusiven Besitz. Möchte er geteilten Besitz, kann er umwandeln: std::shared_ptr sp = factory();.

factory_unique_ptr.cpp
#include <memory>
#include <iostream>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Factory — caller owns the result
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
    if (type == "circle")    return std::make_unique<Circle>(a);
    if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
    return nullptr;  // unknown shape
}

int main() {
    auto s = make_shape("circle", 5.0);
    if (s) {
        std::cout << s->name() << " area: " << s->area() << '\n';
    }
    // s automatically deleted here
}
Stolperfalle

Übergib einen unique_ptr niemals per const-Referenz an eine Funktion, die das Objekt nur beobachtet — übergib stattdessen einen rohen Zeiger oder eine Referenz. Ein unique_ptr-Parameter signalisiert Besitzübertragung.

- void use(Widget* w) — beobachtet, kein Besitz
- void sink(std::unique_ptr w) — übernimmt den Besitz (Aufruf mit std::move)
- void reseat(std::unique_ptr& w) — darf das besessene Objekt ersetzen

Konstruiere niemals zwei unique_ptr aus demselben rohen Zeiger — beide rufen delete auf, was zu doppeltem Freigeben führt. Genau deshalb ist make_unique vorzuziehen: Es gibt gar keinen rohen Zeiger, den man versehentlich wiederverwenden könnte.

Best Practice

1. Nimm standardmäßig unique_ptr — er sollte deine erste Wahl für Heap-Allokation sein.
2. Verwende immer make_unique — schreibe nie unique_ptr(new T(...)).
3. Gib unique_ptr aus Factorys zurück — er wandelt sich implizit in einen shared_ptr um, falls der Aufrufer das braucht.
4. Übergib per Wert, um den Besitz zu übertragen — void sink(unique_ptr p). Der Aufrufer muss std::move verwenden.
5. Übergib einen rohen Zeiger bzw. eine Referenz zum Beobachten — void use(const T& obj) oder void use(T* obj).
6. Markiere Verschiebekonstruktor und -zuweisung als noexcept — entscheidend für Typen, die in Containern liegen.

Wichtigste Erkenntnisse
  • unique_ptr bietet exklusiven Besitz ohne Overhead gegenüber rohen Zeigern
  • Erzeuge ihn immer mit std::make_unique(args...) — nie mit rohem new
  • Nicht kopierbar, aber verschiebbar — der Besitz wandert per std::move
  • Eigene Deleter ermöglichen RAII für C-Ressourcen (FILE*, Sockets usw.)
  • Gib unique_ptr aus Factory-Funktionen zurück — er wandelt sich bei Bedarf in einen shared_ptr um
  • Übergib einen rohen Zeiger oder eine Referenz, wenn eine Funktion nur beobachtet und nicht besitzt

Quiz — Teste dein Wissen

(15 XP)

1. Wie hoch ist der Overhead von `std::unique_ptr<T>` gegenüber einem rohen `T*` (mit Standard-Deleter)?

2. Wie übergibst du einen `unique_ptr` an eine Funktion, die den Besitz übernehmen soll?

3. Warum ist `std::make_unique` besser als `unique_ptr<T>(new T(...))`?