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

shared_ptr, slow_ptr & γραφήματα ιδιοκτησίας

Κατανοήστε την κοινόχρηστη ιδιοκτησία με καταμέτρηση αναφορών με το shared_ptr, διακόψτε τους κύκλους με το slow_ptr, μάθετε για τα μπλοκ ελέγχου και το ψευδώνυμο και μάθετε πότε το shared_ptr είναι η λάθος επιλογή.

shared_ptr: Κοινή ιδιοκτησία

Τοstd::shared_ptrενεργοποιεί την κοινή ιδιοκτησία — πολλές παρουσίεςshared_ptrμπορούν να κατέχουν το ίδιο αντικείμενο. Το αντικείμενο καταστρέφεται όταν καταστρέφεται ή μηδενίζεται το τελευταίο που κατέχει τοshared_ptr.

Εσωτερικά, τοshared_ptrδιατηρεί ένα μπλοκ ελέγχου που περιέχει:
- Ένας ισχυρός αριθμός αναφορών (αριθμός κατόχωνshared_ptr)
- Ένας αδύναμος αριθμός αναφορών (αριθμός παρατηρητώνweak_ptr)
- Ο διαγραφέας και ο εκχωρητής

Τοshared_ptrείναι μεγαλύτερο από έναν ακατέργαστο δείκτη (συνήθως 2 δείκτες: ένας στο αντικείμενο, ένας στο μπλοκ ελέγχου) και η μέτρηση αναφοράς έχει ατομική επιβάρυνση σε κάθε αντίγραφο/καταστροφή. Χρησιμοποιήστε το μόνο όταν η ιδιοκτησία είναι πραγματικά κοινή.

make_shared και Reference Counting

Τοstd::make_shared(args...)εκχωρεί το μπλοκ αντικειμένου ΚΑΙ ελέγχου σε ενιαία κατανομή (αντί για δύο). Αυτό είναι πιο φιλικό στην κρυφή μνήμη και αποφεύγει το ζήτημα ασφάλειας εξαίρεσης του ακατέργαστουnew.

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

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

ضعیف_ptr: Κύκλοι διακοπής

Τοstd::weak_ptrπαρατηρεί ένα αντικείμενο που διαχειρίζεται τοshared_ptrχωρίς να παρατείνει τη διάρκεια ζωής του. Για πρόσβαση στο αντικείμενο, καλέστε τοlock(), το οποίο επιστρέφει έναshared_ptr— ή τοnullptrεάν το αντικείμενο έχει ήδη καταστραφεί. Τοweak_ptrείναι απαραίτητο για την παραβίαση κυκλικών αναφορών που διαφορετικά θα διέρρεαν.

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

struct Employee;
struct Team {
    std::string name;
    std::vector<std::shared_ptr<Employee>> members;
    Team(std::string n) : name(std::move(n)) {}
    ~Team() { std::cout << "Team " << name << " destroyed\n"; }
};

struct Employee {
    std::string name;
    std::weak_ptr<Team> team;  // weak — does NOT own the team
    Employee(std::string n) : name(std::move(n)) {}
    ~Employee() { std::cout << "Employee " << name << " destroyed\n"; }

    void print_team() {
        if (auto t = team.lock()) {  // try to get shared_ptr
            std::cout << name << " belongs to " << t->name << '\n';
        } else {
            std::cout << name << " has no team\n";
        }
    }
};

int main() {
    auto team = std::make_shared<Team>("Engineering");
    auto alice = std::make_shared<Employee>("Alice");
    auto bob = std::make_shared<Employee>("Bob");

    team->members.push_back(alice);
    team->members.push_back(bob);
    alice->team = team;  // weak_ptr — no circular ownership
    bob->team = team;

    alice->print_team();  // "Alice belongs to Engineering"

    team.reset();  // destroy team
    alice->print_team();  // "Alice has no team" — weak_ptr expired
}
Παγίδα

Εάν δύο αντικείμενα συγκρατούν τοshared_ptrμεταξύ τους, κανένα δεν μπορεί ποτέ να φτάσει στο μηδέν μέτρηση αναφοράς — κρατούν το ένα το άλλο ζωντανό για πάντα. Πρόκειται για διαρροή μνήμης, παρόλο που χρησιμοποιείτε έξυπνους δείκτες.

Δείκτης ``cpp
struct Node {
std::shared_ptr next; // if next points back, LEAK!
};
auto a = std::make_shared();
auto b = std::make_shared();
a->next = b;
b->next = a; // circular — neither a nor b will ever be destroyed
``

The fix: make one direction a weak_ptr. In tree structures, children hold weak_ptr to parents (or raw observer pointers). In doubly-linked lists, use weak_ptr for the prev.

The Aliasing Constructor & enable_shared_from_this

Ο κατασκευαστής aliasing δημιουργεί έναshared_ptrπου μοιράζεται την ιδιοκτησία με ένα υπάρχονshared_ptrαλλά δείχνει σε ένα διαφορετικό αντικείμενο (συνήθως ένα μέλος). Το ψευδώνυμο αντικείμενο παραμένει ζωντανό όσο υπάρχει οποιοδήποτε ψευδώνυμοshared_ptr:

Το ``cpp
struct Pair { int first; int second; };
auto p = std::make_shared(Pair{1, 2});
std::shared_ptr sp_first(p, &p->first); // shares p's control block
// p can go out of scope — Pair stays alive because sp_first still references it
``

enable_shared_from_this is a CRTP base class that allows an object managed by shared_ptr to obtain a shared_ptr to itself via shared_from_this(). Without it, constructing a new shared_ptr from this δημιουργεί ένα δεύτερο μπλοκ ελέγχου, που οδηγεί σε διπλό ελεύθερο.

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

Τοshared_ptrχρησιμοποιείται υπερβολικά. Προσεγγίστε το μόνο όταν η ιδιοκτησία είναι πραγματικά κοινή σε ανεξάρτητες ζωές. ΜΗΝ χρησιμοποιείτε τοshared_ptrως τεμπέλικο υποκατάστατο για να σκεφτείτε την ιδιοκτησία.

Αποφύγετε τοshared_ptrόταν:
- Υπάρχει ένας μόνο κάτοχος — χρησιμοποιήστε τοunique_ptrαντ' αυτού (μηδενικό κόστος).
- Το αντικείμενο ζει στη στοίβα — απλώς χρησιμοποιήστε το αντικείμενο απευθείας.
- Απλώς πρέπει να περάσετε μια αναφορά — περάστεT&ήT*.
- Το χρησιμοποιείτε "επειδή είναι ασφαλές" — Τοshared_ptrέχει κόστος χρόνου εκτέλεσης (ατομική επαναμέτρηση, επιπλέον κατανομή) και κρύβει τη σημασιολογία ιδιοκτησίας.
- Σε κρίσιμες για την απόδοση μονοπάτια — οι πράξεις ατομικής επανακαταμέτρησης είναι εκπληκτικά ακριβές υπό αμφισβήτηση.

Νόμιμα σενάρια κοινής ιδιοκτησίας: κρυφές μνήμες (πολλαπλές αναζητήσεις επιστρέφουν το ίδιο αντικείμενο), συστήματα pub/sub, κοινόχρηστα αντικείμενα διαμόρφωσης και δομές δεδομένων γραφημάτων με πραγματικά κοινόχρηστους κόμβους.

Εσωτερικά μπλοκ ελέγχου

Η κατανόηση του μπλοκ ελέγχου βοηθά στην εξήγηση του κόστουςshared_ptr. Τοmake_sharedσυνδυάζει το αντικείμενο και το μπλοκ ελέγχου σε μία κατανομή. Η κατασκευή από ακατέργαστοnewαπαιτεί δύο ξεχωριστές εκχωρήσεις.

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
Βασικά συμπεράσματα
  • Τοshared_ptrχρησιμοποιεί μέτρηση αναφορών — το αντικείμενο πεθαίνει όταν φύγει ο τελευταίος κάτοχος
  • Τοmake_sharedεκχωρεί αντικείμενο + μπλοκ ελέγχου σε μία κατανομή (προτιμήστε το πάντα)
  • Τοweak_ptrπαρατηρεί χωρίς να κατέχει — χρησιμοποιήστε τοlock()για ασφαλή πρόσβαση, σπάει τις κυκλικές αναφορές
  • Τοshared_ptrέχει πλάτος 2 δείκτες και έχει επιβάρυνση ατομικής επαναμέτρησης — όχι δωρεάν
  • Η προεπιλογή είναιunique_ptr. χρησιμοποιήστε τοshared_ptrμόνο όταν η ιδιοκτησία είναι πραγματικά κοινή
  • Μην κατασκευάζετε ποτέ δύοshared_ptrαπό τον ίδιο πρωτογενή δείκτη — χρησιμοποιήστε τοenable_shared_from_thisγια αυτοαναφορές

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

(15 XP)

1. Τι συμβαίνει όταν δύο αντικείμενα συγκρατούν το`shared_ptr`μεταξύ τους;

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

3. Πώς έχετε πρόσβαση με ασφάλεια σε ένα αντικείμενο μέσω ενός`weak_ptr`;