सामग्री पर जाएं

shared_ptr, weak_ptr और ओनरशिप ग्राफ़

shared_ptr के साथ रेफ़रेंस-काउंटेड साझा ओनरशिप को समझें, weak_ptr से चक्र (cycle) तोड़ना सीखें, कंट्रोल ब्लॉक और एलियासिंग के बारे में जानें, और यह भी जानें कि shared_ptr कब गलत विकल्प होता है।

shared_ptr: साझा ओनरशिप

std::shared_ptr साझा ओनरशिप (shared ownership) संभव बनाता है — कई shared_ptr इंस्टेंस एक ही ऑब्जेक्ट के मालिक हो सकते हैं। ऑब्जेक्ट तब नष्ट होता है जब आख़िरी स्वामी shared_ptr नष्ट या रीसेट होता है।

अंदरूनी तौर पर shared_ptr एक कंट्रोल ब्लॉक (control block) रखता है, जिसमें ये होते हैं:
- एक स्ट्रॉन्ग रेफ़रेंस काउंट (shared_ptr मालिकों की संख्या)
- एक वीक रेफ़रेंस काउंट (weak_ptr ऑब्ज़र्वर की संख्या)
- डिलीटर और एलोकेटर

shared_ptr रॉ पॉइंटर से बड़ा होता है (आमतौर पर 2 पॉइंटर: एक ऑब्जेक्ट के लिए, एक कंट्रोल ब्लॉक के लिए), और रेफ़रेंस काउंटिंग में हर कॉपी/डिस्ट्रक्शन पर एटॉमिक ओवरहेड होता है। इसका उपयोग तभी करें जब ओनरशिप वास्तव में साझा हो।

make_shared और रेफ़रेंस काउंटिंग

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

weak_ptr: चक्र तोड़ना

std::weak_ptr, shared_ptr द्वारा मैनेज किए जा रहे ऑब्जेक्ट को उसकी लाइफ़टाइम बढ़ाए बिना देखता है। ऑब्जेक्ट तक पहुँचने के लिए lock() कॉल करें, जो एक shared_ptr रिटर्न करता है — या अगर ऑब्जेक्ट पहले ही नष्ट हो चुका हो तो nullptr। वृत्ताकार रेफ़रेंस (circular reference) को तोड़ने के लिए 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 रखते हैं, तो कोई भी रेफ़रेंस काउंट शून्य तक कभी नहीं पहुँच सकता — वे एक-दूसरे को हमेशा जीवित रखते हैं। यह मेमोरी लीक है, भले ही आप स्मार्ट पॉइंटर इस्तेमाल कर रहे हों।

struct Node {
    std::shared_ptr<Node> next;  // अगर next वापस इंगित करे, तो LEAK!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // वृत्ताकार — न a और न b कभी नष्ट होंगे

समाधान: एक दिशा को weak_ptr बना दें। ट्री स्ट्रक्चर में, चाइल्ड अपने पैरेंट के लिए weak_ptr (या रॉ ऑब्ज़र्वर पॉइंटर) रखते हैं। डबली-लिंक्ड लिस्ट में, prev पॉइंटर के लिए weak_ptr का उपयोग करें।

एलियासिंग कंस्ट्रक्टर और enable_shared_from_this

एलियासिंग कंस्ट्रक्टर एक ऐसा shared_ptr बनाता है जो किसी मौजूदा shared_ptr के साथ ओनरशिप साझा करता है, लेकिन किसी दूसरे ऑब्जेक्ट (आमतौर पर किसी member) की ओर इंगित करता है। जब तक कोई भी एलियास्ड shared_ptr मौजूद है, मूल ऑब्जेक्ट जीवित रहता है:

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first);  // p का कंट्रोल ब्लॉक साझा करता है
// p स्कोप से बाहर जा सकता है — Pair जीवित रहता है क्योंकि sp_first अब भी उसे रेफ़र करता है

enable_shared_from_this एक CRTP बेस क्लास है, जो shared_ptr से मैनेज किए जा रहे ऑब्जेक्ट को shared_from_this() के ज़रिए अपने ही shared_ptr प्राप्त करने देती है। इसके बिना, this से नया shared_ptr बनाने पर दूसरा कंट्रोल ब्लॉक बन जाता है, जिससे डबल-फ़्री होता है।

सर्वोत्तम अभ्यास

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. `shared_ptr<T>(new T(...))` की तुलना में `std::make_shared` को प्राथमिकता क्यों दी जाती है?

3. `weak_ptr` के ज़रिए किसी ऑब्जेक्ट को सुरक्षित रूप से कैसे एक्सेस करें?