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

std::unique_ptr: विशिष्ट ओनरशिप

जानें कि std::unique_ptr कैसे बिना किसी ओवरहेड के विशिष्ट ओनरशिप देता है, make_unique का उपयोग कैसे करें, C रिसोर्स के लिए कस्टम डिलीटर, और फ़ैक्ट्री फ़ंक्शन पैटर्न।

unique_ptr क्या है?

std::unique_ptr एक स्मार्ट पॉइंटर (smart pointer) है जो किसी ऑब्जेक्ट का मालिक बनकर उस पर विशिष्ट अधिकार रखता है — किसी भी ऑब्जेक्ट की ओर ठीक एक ही unique_ptr इंगित करता है। जब unique_ptr नष्ट होता है (स्कोप से बाहर जाता है), तो वह अपने स्वामित्व वाले ऑब्जेक्ट को अपने-आप delete कर देता है।

मुख्य विशेषताएँ:
- नॉन-कॉपीएबल — कॉपी करने से दो मालिक बन जाएँगे, जो विशिष्ट ओनरशिप का उल्लंघन है।
- मूवेबल — ओनरशिप std::move के ज़रिए ट्रांसफ़र की जा सकती है।
- शून्य ओवरहेड — sizeof(unique_ptr) == sizeof(T*) (डिफ़ॉल्ट डिलीटर के साथ)। जनरेट किया गया कोड मैन्युअल delete के साथ रॉ पॉइंटर के उपयोग जैसा ही होता है।
- यही डिफ़ॉल्ट स्मार्ट पॉइंटर है। पहले unique_ptr अपनाएँ; shared_ptr तभी इस्तेमाल करें जब आपको वास्तव में साझा ओनरशिप की ज़रूरत हो।

make_unique: एकमात्र सही तरीका

unique_ptr बनाने का एकमात्र सही तरीका std::make_unique(args...) (C++14) है। यह एक्सेप्शन-सेफ़ है, संक्षिप्त है, और 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 इन्हें संभालने के लिए कस्टम डिलीटर को सपोर्ट करता है। डिलीटर टाइप का हिस्सा बन जाता है, इसलिए स्टेटलेस लैम्ब्डा (या कोई खाली फ़ंक्टर) इस्तेमाल करने पर कोई ओवरहेड नहीं होता। फ़ंक्शन पॉइंटर को डिलीटर बनाने पर वह पॉइंटर 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 शून्य-इनिशियलाइज़ किए गए int
arr[0] = 42;

हालाँकि, std::vector लगभग हमेशा बेहतर विकल्प है — उसे अपना साइज़ पता होता है, वह इटरेशन सपोर्ट करता है, और उतना ही कुशल है। unique_ptr का उपयोग केवल तब करें जब आप C API के साथ इंटरफ़ेस कर रहे हों, या आपको ऐसा बफ़र चाहिए जिसका साइज़ बदला न जा सके और जिसे कॉपी न किया जा सके।

unique_ptr रिटर्न करने वाले फ़ैक्ट्री फ़ंक्शन

फ़ैक्ट्री फ़ंक्शन से unique_ptr रिटर्न करना C++ का मानक (idiomatic) ओनरशिप पैटर्न है। कॉलर को विशिष्ट ओनरशिप मिलती है। अगर कॉलर साझा ओनरशिप चाहता है, तो वह कन्वर्ट कर सकता है: 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
}
सामान्य गलती

ऐसे फ़ंक्शन में, जो ऑब्जेक्ट को सिर्फ़ देखता है, unique_ptr को const रेफ़रेंस से कभी पास न करें — उसकी जगह रॉ पॉइंटर या रेफ़रेंस पास करें। unique_ptr पैरामीटर ओनरशिप के ट्रांसफ़र का संकेत देते हैं।

- void use(Widget* w) — सिर्फ़ देखता है, ओनरशिप नहीं
- void sink(std::unique_ptr w) — ओनरशिप ले लेता है (std::move के साथ कॉल करें)
- void reseat(std::unique_ptr& w) — स्वामित्व वाले ऑब्जेक्ट को बदल सकता है

एक ही रॉ पॉइंटर से दो unique_ptr कभी न बनाएँ — दोनों delete कॉल करेंगे, जिससे डबल-फ़्री होगा। इसीलिए make_unique को प्राथमिकता दी जाती है: उसमें ऐसा कोई रॉ पॉइंटर होता ही नहीं जिसे गलती से दोबारा इस्तेमाल किया जा सके।

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

1. डिफ़ॉल्ट रूप से unique_ptr अपनाएँ — हीप एलोकेशन के लिए यही आपकी पहली पसंद होनी चाहिए।
2. हमेशा make_unique का उपयोग करें — unique_ptr(new T(...)) कभी न लिखें।
3. फ़ैक्ट्री से unique_ptr रिटर्न करें — अगर कॉलर को ज़रूरत हो, तो यह इम्प्लिसिट रूप से shared_ptr में बदल जाता है।
4. ओनरशिप ट्रांसफ़र करने के लिए वैल्यू द्वारा पास करें — void sink(unique_ptr p)। कॉलर को std::move का उपयोग करना होगा।
5. सिर्फ़ देखने के लिए रॉ पॉइंटर/रेफ़रेंस पास करें — void use(const T& obj) या void use(T* obj)।
6. मूव कंस्ट्रक्टर/असाइनमेंट को noexcept चिह्नित करें — कंटेनर में रखे जाने वाले टाइप के लिए यह बेहद ज़रूरी है।

मुख्य बातें
  • unique_ptr रॉ पॉइंटर की तुलना में शून्य ओवरहेड के साथ विशिष्ट ओनरशिप देता है
  • हमेशा std::make_unique(args...) से बनाएँ — रॉ new कभी इस्तेमाल न करें
  • कॉपी नहीं हो सकता, लेकिन मूव हो सकता है — ओनरशिप std::move से ट्रांसफ़र होती है
  • कस्टम डिलीटर C रिसोर्स (FILE*, सॉकेट आदि) के लिए RAII को संभव बनाते हैं
  • फ़ैक्ट्री फ़ंक्शन से unique_ptr रिटर्न करें — ज़रूरत पड़ने पर वह shared_ptr में बदल जाता है
  • जब फ़ंक्शन सिर्फ़ देखता हो, मालिक न हो, तब रॉ पॉइंटर या रेफ़रेंस पास करें

क्विज़ — अपना ज्ञान परखें

(15 XP)

1. रॉ `T*` (डिफ़ॉल्ट डिलीटर के साथ) की तुलना में `std::unique_ptr<T>` का ओवरहेड क्या है?

2. जिस फ़ंक्शन को ओनरशिप लेनी है, उसे `unique_ptr` कैसे पास करना चाहिए?

3. `unique_ptr<T>(new T(...))` की तुलना में `std::make_unique` को प्राथमिकता क्यों दी जाती है?