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

कॉपी सिमेंटिक्स: डीप बनाम शैलो

शैलो और डीप कॉपी का अंतर, रिसोर्स के मालिक क्लास के लिए सही कॉपी ऑपरेशन लागू करने का तरीका, और copy-and-swap idiom सीखें।

कॉपी सिमेंटिक्स क्यों मायने रखती है

जब आप C++ में किसी ऑब्जेक्ट की कॉपी बनाते हैं, तो ठीक-ठीक क्या कॉपी होता है? सरल टाइप के लिए उत्तर सीधा है — सभी मेंबर बिट-दर-बिट कॉपी होते हैं। लेकिन जब कोई ऑब्जेक्ट पॉइंटर के ज़रिए किसी रिसोर्स का मालिक होता है, तो डिफ़ॉल्ट (कंपाइलर-जनरेटेड) कॉपी कंस्ट्रक्टर शैलो कॉपी (shallow copy) करता है: वह पॉइंटर का मान कॉपी करता है, लेकिन उस डेटा को नहीं जिसकी ओर वह इशारा करता है। इसका मतलब है कि अब दो ऑब्जेक्ट एक ही रिसोर्स साझा करते हैं, जिससे double-free बग और डेटा करप्शन होता है।

कॉपी सिमेंटिक्स समझना ज़रूरी है, क्योंकि कॉपी कई जगह अपने आप होती है: वैल्यू से पास करने पर, फ़ंक्शन से रिटर्न करने पर, कंटेनर में डालने पर और असाइनमेंट में।

शैलो कॉपी की समस्या

कंपाइलर-जनरेटेड कॉपी कंस्ट्रक्टर हर मेंबर को कॉपी करता है। पॉइंटर के लिए यह पता (address) कॉपी करता है — पॉइंटर जिस डेटा की ओर इशारा करता है उसे नहीं। यह शैलो कॉपी है और रिसोर्स के मालिक क्लास के लिए खतरनाक है।

shallow_copy_bug.cpp
#include <iostream>
#include <cstring>

class BrokenString {
    char* data_;
    std::size_t len_;

public:
    BrokenString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    ~BrokenString() {
        delete[] data_;  // frees the buffer
    }

    // No copy constructor defined!
    // Compiler generates shallow copy: copies the pointer

    void print() const {
        std::cout << data_ << '\n';
    }
};

int main() {
    BrokenString a{"Hello"};
    {
        BrokenString b = a;  // shallow copy: b.data_ == a.data_
        b.print();           // "Hello" — works for now
    }  // b destroyed: delete[] b.data_ (same pointer as a.data_!)

    // a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
    a.print();  // may crash, print garbage, or appear to work
    return 0;
}

डीप कॉपी लागू करना

डीप कॉपी (deep copy) नई मेमोरी आवंटित करती है और असली डेटा कॉपी करती है। इससे हर ऑब्जेक्ट के पास रिसोर्स की अपनी स्वतंत्र कॉपी रहती है।

deep_copy.cpp
#include <iostream>
#include <cstring>
#include <algorithm>

class SafeString {
    char* data_;
    std::size_t len_;

public:
    explicit SafeString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    // Deep copy constructor
    SafeString(const SafeString& other)
        : len_{other.len_}
        , data_{new char[other.len_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Deep copy assignment with self-assignment check
    SafeString& operator=(const SafeString& other) {
        if (this == &other) return *this;  // self-assignment guard

        // Allocate new resource BEFORE releasing old one
        char* new_data = new char[other.len_ + 1];
        std::strcpy(new_data, other.data_);

        // Release old resource
        delete[] data_;

        // Adopt new resource
        data_ = new_data;
        len_ = other.len_;
        return *this;
    }

    ~SafeString() { delete[] data_; }

    void print() const {
        std::cout << data_ << " (len=" << len_ << ")\n";
    }
};

int main() {
    SafeString a{"Hello"};
    SafeString b = a;     // deep copy: b gets its own buffer
    SafeString c{"World"};
    c = a;                // deep copy assignment
    a.print();  // Hello (len=5)
    b.print();  // Hello (len=5)
    c.print();  // Hello (len=5)
    return 0;
}

Copy-and-Swap Idiom

copy-and-swap idiom कॉपी असाइनमेंट ऑपरेटर का एक सुंदर, एक्सेप्शन-सेफ़ क्रियान्वयन देता है। यह कॉपी कंस्ट्रक्टर और एक swap फ़ंक्शन का दोबारा उपयोग करता है, जिससे कोड की न्यूनतम पुनरावृत्ति के साथ मज़बूत एक्सेप्शन सेफ़्टी मिलती है।

copy_and_swap.cpp
#include <iostream>
#include <cstring>
#include <utility>

class Buffer {
    char* data_;
    std::size_t size_;

public:
    explicit Buffer(std::size_t size)
        : size_{size}, data_{new char[size]{}} {}

    Buffer(const Buffer& other)
        : size_{other.size_}, data_{new char[other.size_]}
    {
        std::memcpy(data_, other.data_, size_);
    }

    ~Buffer() { delete[] data_; }

    // Copy-and-swap: take parameter by VALUE (invokes copy ctor)
    Buffer& operator=(Buffer other) {   // copy happens here
        swap(*this, other);             // swap resources
        return *this;
    }   // 'other' destroyed here, freeing our old resource

    // Non-throwing swap — essential for strong exception safety
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    char& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return size_; }
};

int main() {
    Buffer a{5};
    a[0] = 'H'; a[1] = 'i';

    Buffer b{10};
    b = a;  // copy-and-swap: exception-safe, self-assignment-safe

    std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
    return 0;
}

कॉपी को अक्षम करना

कुछ टाइप कॉपी करने योग्य नहीं होने चाहिए — उदाहरण के लिए वे टाइप जो अनन्य रिसोर्स दर्शाते हैं, जैसे फ़ाइल हैंडल, नेटवर्क कनेक्शन या थ्रेड। इसे साफ़ बताने और लागू करने के लिए =delete इस्तेमाल करें:

class UniqueConnection {
public:
    UniqueConnection(const UniqueConnection&) = delete;
    UniqueConnection& operator=(const UniqueConnection&) = delete;
    // मूव ऑपरेशन फिर भी सक्षम किए जा सकते हैं
};

C++11 से पहले, यह कॉपी ऑपरेशन को बिना डेफ़िनिशन के private डिक्लेयर करके किया जाता था — एक भ्रमित करने वाला जुगाड़, जिसकी जगह =delete सुंदर ढंग से ले लेता है।

सामान्य गलती

एक सीधा-सादा (naive) कॉपी असाइनमेंट, जो नया रिसोर्स आवंटित करने से पहले पुराना delete कर देता है, दो तरह से गलत है: (1) सेल्फ़-असाइनमेंट (a = a) डेटा को delete कर देता है और फिर उसी से कॉपी करने की कोशिश करता है, और (2) अगर delete के बाद new एक्सेप्शन फेंके, तो ऑब्जेक्ट अमान्य अवस्था में रह जाता है। copy-and-swap idiom दोनों समस्याएँ हल करता है: कॉपी किसी भी बदलाव से पहले की जाती है, और swap noexcept होता है।

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

कॉपी बग से बचने का सबसे अच्छा तरीका है कभी रॉ रिसोर्स को सीधे न संभालना। char* की जगह std::string, रॉ ऐरे की जगह std::vector, और मालिकाना रॉ पॉइंटर की जगह std::unique_ptr इस्तेमाल करें। जब आपके सभी मेंबर की कॉपी सिमेंटिक्स सही हो, तो कंपाइलर-जनरेटेड कॉपी ऑपरेशन अपने आप सही होते हैं। व्यवहार में यही Rule of Zero है।

मुख्य बातें
  • कंपाइलर-जनरेटेड कॉपी शैलो होती है — यह पॉइंटर के मान कॉपी करती है, पॉइंटर जिस डेटा की ओर इशारा करता है उसे नहीं
  • रिसोर्स के मालिक क्लास को डीप कॉपी कंस्ट्रक्टर और असाइनमेंट ऑपरेटर चाहिए
  • copy-and-swap idiom आपको एक्सेप्शन सेफ़्टी और सेल्फ़-असाइनमेंट सेफ़्टी मुफ़्त में देता है
  • अनन्य-रिसोर्स वाले टाइप की कॉपी को स्पष्ट रूप से प्रतिबंधित करने के लिए =delete इस्तेमाल करें
  • सबसे अच्छी रणनीति Rule of Zero है: RAII रैपर इस्तेमाल करें ताकि आपको कस्टम कॉपी लॉजिक कभी न लिखना पड़े

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

(15 XP)

1. जो ऑब्जेक्ट रॉ पॉइंटर के ज़रिए हीप मेमोरी का मालिक है, उसकी शैलो कॉपी करने पर क्या होता है?

2. copy-and-swap idiom पैरामीटर को वैल्यू से क्यों लेता है?

3. कस्टम कॉपी ऑपरेशन की ज़रूरत को पूरी तरह टालने के लिए कौन-सा तरीका अनुशंसित है?