विषय पर बढ़ें

अंतिम अद्यतन:2026-10-12

C++ copy-and-swap इडियम

हमें copy तथा swap इडियम की आवश्यकता क्यों है?

संसाधन प्रबंधित करने वाली कोई भी क्लास (रैपर, जैसे स्मार्ट पॉइंटर) को big three कार्यान्वित करने की आवश्यकता होती है। यद्यपि कॉपी कंस्ट्रक्टर तथा डिस्ट्रक्टर का लक्ष्य और कार्यान्वयन सरल है।

परंतु कॉपी असाइनमेंट (copy assignment) ऑपरेटर निस्संदेह सबसे सूक्ष्म तथा सबसे कठिन है।

क्या करना चाहिए? किस जाल (pitfall) से बचना है?

copy-swap इसका समाधान है, जो असाइनमेंट ऑपरेटर को दो कामों में भली-भाँति सहायता देता है: कोड-दोहराव से बचाव, तथा प्रबल exception guarantee देना।

यह कैसे काम करता है?

संकल्पनात्मक रूप से, यह कॉपी कंस्ट्रक्टर की क्षमता से डेटा का स्थानीय प्रतिलिपि बनाकर काम करता है, फिर स्वैप कार्यक्षमता से प्रतिलिपि-डेटा लेकर पुराने डेटे को नए डेटे से बदल देता है। फिर अस्थायी प्रतिलिपि नष्ट हो जाती है और पुराना डेटा अपने साथ ले जाती है। हमारे पास नए डेटे की प्रतिलिपि बचती है।

copy-swap के उपयोग के लिए हमें तीन चीज़ें चाहिए:

  • एक वैध कॉपी कंस्ट्रक्टर
  • एक वैध डिस्ट्रक्टर (दोनों किसी भी रैपर की आधारशिला हैं, इसलिए वैसे भी पूर्ण होने ही चाहिए) तथा स्वैप कार्यक्षमता।

स्वैप फंक्शन एक ऐसा फंक्शन है जो अपवाद नहीं फेंकता, और किसी क्लास के दो ऑब्जेक्ट्स या मेंबर्स की अदला-बदली करता है। हमारा मन कर सकता है कि अपनी विधि देने के बजाय std::swap ही उपयोग कर लें, पर यह संभव नहीं है। std::swap अपने कार्यान्वयन में copy-constructor तथा copy-assignment ऑपरेटर उपयोग करता है, और हम अंततः असाइनमेंट ऑपरेटर को उसी के आधार पर परिभाषित करने का प्रयास कर बैठेंगे!

(इतना ही नहीं, swap का बिना शर्त कॉल हमारे कस्टम swap ऑपरेटर को उपयोग करेगा, जिससे std::swap से होने वाली अनावश्यक क्लास-निर्माण तथा विनाश छूट जाएँगे।)

ठोस उदाहरण इस प्रकार है:

namespace A {
    template<typename T>
    class smart_ptr {
    public:
        smart_ptr() noexcept : ptr_(new T()) {

        }

        smart_ptr(const T &ptr) noexcept : ptr_(new T(ptr)) {

        }

        smart_ptr(smart_ptr &rhs) noexcept {
            ptr_ = rhs.release();       // 释放所有权,此时rhs的ptr_指针为nullptr
        }

        void swap(smart_ptr &rhs) noexcept { // noexcept == throw() 保证不抛出异常
            using std::swap;
            swap(ptr_, rhs.ptr_);
        }

        T *release() noexcept {
            T *ptr = ptr_;
            ptr_ = nullptr;
            return ptr;
        }

        T *get() const noexcept {
            return ptr_;
        }

    private:
        T *ptr_;
    };

// 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
    template<typename T>
    void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
        lhs.swap(rhs);
    }
}
// 注释开启,会引发ADL冲突
//namespace std {
//    // 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
//    template<typename T>
//    void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
//        lhs.swap(rhs);
//    }
//
//}

int main() {

    using std::swap;
    A::smart_ptr<std::string> s1("hello"), s2("world");
    // 交换前
    std::cout << *s1.get() << " " << *s2.get() << std::endl;
    swap(s1, s2);      // 这里swap 能够通过Koenig搜索或者说ADL根据s1与s2的命名空间来查找swap函数
    // 交换后
    std::cout << *s1.get() << " " << *s2.get() << std::endl;
    s1=s2;
}

अब उपरोक्त s1=s2 को कार्य कराने के लिए, असाइनमेंट ऑपरेटर कार्यान्वित करना आवश्यक है।

विधि 1

स्व-असाइनमेंट (self-assignment) से बचने के लिए, प्रायः नीचे जैसी लिखावट अपनाई जाती है।

अच्छा नहीं!

exception-safety नहीं, केवल स्व-असाइनमेंट सुरक्षा है

smart_ptr &operator=(const smart_ptr &rhs) {
    if (*this != rhs) {
        delete ptr_;
        ptr_ = new T(rhs.ptr_);  // 当new 发生异常,此时ptr_指向的而是一块被删除区域,而不是被赋值对象的区域
        return *this;
    }
    return *this;
}

विधि 2

यदि new में अपवाद आए, तो ptr_ मूल अवस्था में रहेगा! स्व-असाइनमेंट भी संभाल लेता है! फिर भी पर्याप्त अच्छा नहीं!

इससे कोड स्फीत (bloat) हो जाता है, और एक और समस्या खड़ी होती है: कोड अतिरेक (redundancy)

// 方法2:如果new出现异常,ptr_会保持原装!  也可以处理自我赋值! 还是不够好!
smart_ptr &operator=(const smart_ptr &rhs) {
    T *origin = ptr_;
    ptr_ = new T(rhs.ptr_);
    delete origin;
    return *this;
}

विधि 3

copy and swap — बहुत अच्छा!

smart_ptr &operator=(smart_ptr &rhs) noexcept {
    smart_ptr tmp(rhs);
    swap(tmp);
    return *this;
}

विधि 4

पास-बाय-वैल्यू कर दें — विधि 3 जैसा ही है!

smart_ptr &operator=(smart_ptr rhs) noexcept {
    swap(rhs);
    return *this;
}

C++11 move

big three में move ctor तथा move assignment जोड़ने से big five बन जाता है।

अब उपरोक्त कोड को पुनः विस्तारित करते हैं:

// move ctor
smart_ptr(smart_ptr &&rhs) noexcept {
    std::cout << "move ctor" << std::endl;
    ptr_ = rhs.ptr_;
    if (ptr_)
        rhs.ptr_ = nullptr;
}

// move assignment
smart_ptr &operator=(smart_ptr &&rhs) noexcept {
    std::cout << "move assignment" << std::endl;
    smart_ptr tmp(rhs);
    swap(rhs);
    return *this;
}

वास्तव में, हमें उसके लिए अतिरिक्त move assignment कोड लिखने की आवश्यकता नहीं — copy-and-swap तथा move-and-swap तकनीक एक ही फंक्शन साझा करती हैं। जब copy कंस्ट्रक्टर उपरोक्त विधि 4 जैसा हो, तो C++11 के लिए, कंपाइलर पैरामीटर के बाएँ-मान (lvalue) या दाएँ-मान (rvalue) होने के आधार पर कॉपी कंस्ट्रक्टर तथा मूव कंस्ट्रक्टर में से चुनता है:

smart_ptr &operator=(smart_ptr rhs) noexcept {
    swap(rhs);
    return *this;
}

अतः जब यह आगे लिखे गए

smart_ptr &operator=(smart_ptr &&rhs) noexcept{}

के साथ साथ रहता है, तब error: ambiguous overload for 'operator=' आ जाती है।

कॉल स्थल इस प्रकार है:

A::smart_ptr<std::string> s1("hello"), s2("world");
A::smart_ptr<std::string> s3 = s1;
A::smart_ptr<std::string> s4 = std::move(s1);
  • यदि s3 = s1 हो, तो other को आरंभीकृत करने के लिए कॉपी कंस्ट्रक्टर कॉल होगा (क्योंकि s1 बायाँ-मान है); असाइनमेंट ऑपरेटर नए बने ऑब्जेक्ट से डेटा की अदला-बदली करेगा — गहरी प्रतिलिपि (deep copy)। यही copy and swap इडियम की परिभाषा है: एक प्रतिलिपि बनाना, प्रतिलिपि से डेटा बदलना, तथा प्रतिलिपि को स्कोप समाप्ति पर स्वतः नष्ट होने देना।
  • यदि s4 = std::move(s1) हो, तो rhs को आरंभीकृत करने के लिए मूव कंस्ट्रक्टर कॉल होगा (क्योंकि std::move(s1) दायाँ-मान है), इसलिए यहाँ गहरी प्रतिलिपि नहीं — केवल कुशल डेटा-स्थानांतरण।

इसीलिए इसे "एकीकृत असाइनमेंट ऑपरेटर" भी कहा जा सकता है, क्योंकि यह "कॉपी असाइनमेंट" तथा "मूव असाइनमेंट" को एक में मिला देता है।

संबंधित विषय

टिप्पणियाँ