Μετάβαση στο περιεχόμενο

std::unique_ptr: Αποκλειστική ιδιοκτησία

Μάθετε πώς το std::unique_ptr παρέχει αποκλειστική ιδιοκτησία μηδενικού γενικού κόστους, πώς να χρησιμοποιείτε το make_unique, προσαρμοσμένους διαγραφείς για πόρους C και εργοστασιακά μοτίβα λειτουργιών.

Τι είναι το unique_ptr;

Οstd::unique_ptrείναι ένας έξυπνος δείκτης που κατέχει ένα αντικείμενο αποκλειστικά — υπάρχει ακριβώς έναunique_ptrπου δείχνει σε οποιοδήποτε αντικείμενο. Όταν τοunique_ptrκαταστρέφεται (βγαίνει εκτός πεδίου εφαρμογής), διαγράφει αυτόματα το αντικείμενο που ανήκει.

Βασικές ιδιότητες:
- Μη αντιγραφή — η αντιγραφή θα δημιουργούσε δύο κατόχους, παραβιάζοντας την αποκλειστική ιδιοκτησία.
- Κινητός — η ιδιοκτησία μπορεί να μεταβιβαστεί μέσωstd::move.
- Μηδενικό κόστος —sizeof(unique_ptr) == sizeof(T*)(με προεπιλεγμένο διαγραφέα). Ο κώδικας που δημιουργείται είναι πανομοιότυπος με τη χρήση του πρωτογενούς δείκτη με μη αυτόματη διαγραφή.
- Αυτός είναι ο προεπιλεγμένος έξυπνος δείκτης. Απευθυνθείτε πρώτα στοunique_ptr. χρησιμοποιήστε τοshared_ptrμόνο όταν χρειάζεστε πραγματικά κοινή ιδιοκτησία.

make_unique: Ο μόνος σωστός τρόπος

Τοstd::make_unique(args...)(C++14) είναι ο μόνος σωστός τρόπος δημιουργίας ενόςunique_ptr. Είναι ασφαλές, συνοπτικό και αποφεύγει να γράφειnew.

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"

Προσαρμοσμένοι διαγραφείς για πόρους C

Πολλές βιβλιοθήκες C επιστρέφουν ακατέργαστους δείκτες που πρέπει να ελευθερωθούν με μια συγκεκριμένη συνάρτηση (όχιdelete). Τοunique_ptrυποστηρίζει προσαρμοσμένους διαγραφείς για τον χειρισμό τους. Ο διαγραφέας γίνεται μέρος του τύπου, επομένως έχει μηδενικό κόστος όταν χρησιμοποιείται δείκτης λειτουργίας ή λάμδα χωρίς κατάσταση.

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 με Πίνακες

Τοstd::unique_ptrδιαχειρίζεται έναν δυναμικά εκχωρημένο πίνακα και καλεί αυτόματα τοdelete[]. Παρέχειoperator[]αντί γιαoperator*καιoperator->.

auto arr = std::make_unique<int[]>(100);  // 100 zero-initialized ints
arr[0] = 42;

However, std::vector is almost always preferable — it knows its size, supports iteration, and is just as efficient. Use unique_ptr μόνο κατά τη διασύνδεση με C API ή όταν χρειάζεστε ένα buffer χωρίς δυνατότητα αλλαγής μεγέθους και χωρίς δυνατότητα αντιγραφής.

Factory Functions Returning unique_ptr

Returningunique_ptrfrom factory functions is the idiomatic C++ ownership pattern. The caller receives exclusive ownership. If the caller wants shared ownership, they can convert: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
}
Παγίδα

Never passunique_ptrby const reference to a function that only observes the object — pass a raw pointer or reference instead.unique_ptrparameters signal ownership transfer.

-void use(Widget* w)— observes, no ownership
-void sink(std::unique_ptr w)— takes ownership (call withstd::move)
-void reseat(std::unique_ptr& w)— may replace the owned object

Never construct twounique_ptrfrom the same raw pointer — both will calldelete, causing double-free. This is whymake_uniqueis preferred: there is no raw pointer to accidentally reuse.

Βέλτιστη πρακτική

1. Default tounique_ptr — it should be your first choice for heap allocation.
2. Always usemake_unique — never writeunique_ptr(new T(...)).
3. Returnunique_ptrfrom factories — it implicitly converts toshared_ptrif the caller needs it.
4. Pass by value to transfer ownership —void sink(unique_ptr p). The caller must usestd::move.
5. Pass raw pointer/reference to observe —void use(const T& obj)orvoid use(T* obj).
6. Mark move constructor/assignmentnoexcept — critical for types stored in containers.

Βασικά συμπεράσματα
  • unique_ptrprovides exclusive ownership with zero overhead vs raw pointers
  • Always create viastd::make_unique(args...)— never use rawnew
  • Non-copyable but moveable — ownership transfers viastd::move
  • Οι προσαρμοσμένοι διαγραφείς ενεργοποιούν το RAII για πόρους C (FILE*, υποδοχές κ.λπ.)
  • Επιστρέψτε τοunique_ptrαπό τις εργοστασιακές λειτουργίες — μετατρέπεται σεshared_ptrεάν χρειάζεται
  • Περάστε ακατέργαστο δείκτη ή αναφορά όταν μια συνάρτηση παρατηρεί μόνο, όχι κατέχει

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. Ποιο είναι το γενικό κόστος του`std::unique_ptr<T>`σε σύγκριση με ένα ακατέργαστο`T*`(με προεπιλεγμένο deleter);

2. Πώς πρέπει να μεταβιβάσετε ένα`unique_ptr`σε μια συνάρτηση που πρέπει να αναλάβει την κυριότητα;

3. Γιατί προτιμάται το`std::make_unique`έναντι του`unique_ptr<T>(new T(...))`;