अवलोकन
हमें कॉपी-और-स्वैप मुहावरे की आवश्यकता क्यों है?
कोई भी वर्ग जो एक संसाधन (एक रैपर , जैसे स्मार्ट पॉइंटर) का प्रबंधन करता है , को बिग थ्री को लागू करने की आवश्यकता होती है । हालांकि कॉपी-कंस्ट्रक्टर और डिस्ट्रॉक्टर के लक्ष्य और कार्यान्वयन सीधे होते हैं, लेकिन कॉपी-असाइनमेंट ऑपरेटर यकीनन सबसे अधिक बारीक और कठिन होता है। यह कैसे किया जाना चाहिए? क्या नुकसान से बचने की जरूरत है?
कॉपी-और-स्वैप मुहावरा समाधान है, और सुंदर ढंग से दो बातें प्राप्त करने में सहायता करता है असाइनमेंट ऑपरेटर: परहेज कोड दोहराव है, और एक प्रदान मजबूत अपवाद गारंटी ।
यह कैसे काम करता है?
वैचारिक रूप से , यह डेटा की एक स्थानीय कॉपी बनाने के लिए कॉपी-कंस्ट्रक्टर की कार्यक्षमता का उपयोग करके काम करता है, फिर कॉपी किए गए डेटा को एक swap
फ़ंक्शन के साथ लेता है , नए डेटा के साथ पुराने डेटा को स्वैप करता है। अस्थायी प्रतिलिपि तब नष्ट हो जाती है, पुराने डेटा को अपने साथ ले जाती है। हम नए डेटा की एक प्रति के साथ बचे हैं।
कॉपी-एंड-स्वैप मुहावरे का उपयोग करने के लिए, हमें तीन चीजों की आवश्यकता है: एक काम करने वाला कॉपी-कंस्ट्रक्टर, एक काम करने वाला विध्वंसक (दोनों किसी भी आवरण का आधार हैं, इसलिए वैसे भी पूरा होना चाहिए), और एक swap
फ़ंक्शन।
एक स्वैप फ़ंक्शन एक गैर-फेंकने वाला फ़ंक्शन है जो एक वर्ग की दो वस्तुओं को स्वैप करता है, सदस्य के लिए सदस्य। हम std::swap
अपने स्वयं को प्रदान करने के बजाय उपयोग करने के लिए लुभा सकते हैं , लेकिन यह असंभव होगा; std::swap
इसके कार्यान्वयन के भीतर कॉपी-कंस्ट्रक्टर और कॉपी-असाइनमेंट ऑपरेटर का उपयोग करता है, और हम अंततः स्वयं के संदर्भ में असाइनमेंट ऑपरेटर को परिभाषित करने की कोशिश करेंगे!
(इतना ही नहीं, लेकिन अयोग्य कॉल swap
हमारे कस्टम स्वैप ऑपरेटर का उपयोग करेगा, जो हमारे वर्ग के अनावश्यक निर्माण और विनाश पर लंघन std::swap
करेगा।
गहराई से व्याख्या
लक्ष्य
आइए एक ठोस मामले पर विचार करें। हम प्रबंधन करना चाहते हैं, अन्यथा एक बेकार वर्ग, एक गतिशील सरणी में। हम एक काम करने वाले कंस्ट्रक्टर, कॉपी-कंस्ट्रक्टर और विध्वंसक के साथ शुरू करते हैं:
#include <algorithm> // std::copy
#include <cstddef> // std::size_t
class dumb_array
{
public:
// (default) constructor
dumb_array(std::size_t size = 0)
: mSize(size),
mArray(mSize ? new int[mSize]() : nullptr)
{
}
// copy-constructor
dumb_array(const dumb_array& other)
: mSize(other.mSize),
mArray(mSize ? new int[mSize] : nullptr),
{
// note that this is non-throwing, because of the data
// types being used; more attention to detail with regards
// to exceptions must be given in a more general case, however
std::copy(other.mArray, other.mArray + mSize, mArray);
}
// destructor
~dumb_array()
{
delete [] mArray;
}
private:
std::size_t mSize;
int* mArray;
};
यह वर्ग लगभग सरणी को सफलतापूर्वक प्रबंधित करता है, लेकिन इसे operator=
सही ढंग से काम करने की आवश्यकता है।
एक असफल समाधान
यहाँ बताया गया है कि एक भोली कार्यान्वयन कैसे दिख सकता है:
// the hard part
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get rid of the old data...
delete [] mArray; // (2)
mArray = nullptr; // (2) *(see footnote for rationale)
// ...and put in the new
mSize = other.mSize; // (3)
mArray = mSize ? new int[mSize] : nullptr; // (3)
std::copy(other.mArray, other.mArray + mSize, mArray); // (3)
}
return *this;
}
और हम कहते हैं कि हम समाप्त कर चुके हैं; यह अब लीक के बिना, एक सरणी का प्रबंधन करता है। हालाँकि, यह तीन समस्याओं से ग्रस्त है, जैसा कि कोड में क्रमिक रूप से चिह्नित किया गया है (n)
।
पहला है सेल्फ असाइनमेंट टेस्ट। यह चेक दो उद्देश्यों को पूरा करता है: यह हमें सेल्फ-असाइनमेंट पर अनावश्यक कोड चलाने से रोकने का एक आसान तरीका है, और यह हमें सूक्ष्म कीड़े (जैसे कि केवल इसे हटाने और कॉपी करने के लिए सरणी हटाना) से बचाता है। लेकिन अन्य सभी मामलों में यह केवल कार्यक्रम को धीमा करने के लिए कार्य करता है, और कोड में शोर के रूप में कार्य करता है; स्व-असाइनमेंट शायद ही कभी होता है, इसलिए अधिकांश समय यह चेक एक बेकार है। बेहतर होगा अगर ऑपरेटर इसके बिना ठीक से काम कर सके।
दूसरा यह है कि यह केवल एक मूल अपवाद गारंटी प्रदान करता है। यदि new int[mSize]
विफल रहता है, *this
तो संशोधित किया जाएगा। (अर्थात्, आकार गलत है और डेटा चला गया है!) एक मजबूत अपवाद की गारंटी के लिए, यह कुछ समान होना चाहिए:
dumb_array& operator=(const dumb_array& other)
{
if (this != &other) // (1)
{
// get the new data ready before we replace the old
std::size_t newSize = other.mSize;
int* newArray = newSize ? new int[newSize]() : nullptr; // (3)
std::copy(other.mArray, other.mArray + newSize, newArray); // (3)
// replace the old data (all are non-throwing)
delete [] mArray;
mSize = newSize;
mArray = newArray;
}
return *this;
}
कोड का विस्तार हुआ है! जो हमें तीसरी समस्या की ओर ले जाता है: कोड दोहराव। हमारा असाइनमेंट ऑपरेटर उन सभी कोड को प्रभावी ढंग से डुप्लिकेट करता है जो हमने पहले ही कहीं और लिखे हैं, और यह एक भयानक बात है।
हमारे मामले में, इसका मूल केवल दो लाइनें (आवंटन और प्रतिलिपि) है, लेकिन अधिक जटिल संसाधनों के साथ यह कोड ब्लोट काफी परेशानी भरा हो सकता है। हमें खुद को कभी न दोहराने का प्रयास करना चाहिए।
(किसी को आश्चर्य हो सकता है: यदि एक संसाधन को सही ढंग से प्रबंधित करने के लिए इस कोड की आवश्यकता होती है, तो क्या होगा यदि मेरी कक्षा एक से अधिक का प्रबंधन करती है? जबकि यह एक वैध चिंता का विषय हो सकता है, और वास्तव में इसे गैर-तुच्छ try
/ catch
खंड की आवश्यकता है, यह एक गैर है -इस वजह से एक वर्ग केवल एक संसाधन का प्रबंधन करना चाहिए !
एक सफल समाधान
जैसा कि उल्लेख किया गया है, कॉपी-और-स्वैप मुहावरे इन सभी मुद्दों को ठीक कर देंगे। लेकिन अभी, हमारे पास एक को छोड़कर सभी आवश्यकताएं हैं: एक swap
फ़ंक्शन। जबकि तीन का नियम हमारे कॉपी-कंस्ट्रक्टर, असाइनमेंट ऑपरेटर और विध्वंसक के अस्तित्व को सफलतापूर्वक पूरा करता है, इसे वास्तव में "द बिग थ्री और ए हाफ" कहा जाना चाहिए: किसी भी समय आपकी कक्षा एक संसाधन का प्रबंधन करती है जो swap
फ़ंक्शन प्रदान करने के लिए भी समझ में आता है। ।
हमें अपनी कक्षा में स्वैप कार्यक्षमता जोड़ने की आवश्यकता है, और हम ऐसा करते हैं:
class dumb_array
{
public:
// ...
friend void swap(dumb_array& first, dumb_array& second) // nothrow
{
// enable ADL (not necessary in our case, but good practice)
using std::swap;
// by swapping the members of two objects,
// the two objects are effectively swapped
swap(first.mSize, second.mSize);
swap(first.mArray, second.mArray);
}
// ...
};
( यहाँ स्पष्टीकरण क्यों है public friend swap
।) अब न केवल हम अपने स्वैप कर सकते हैं dumb_array
, लेकिन सामान्य रूप से स्वैप अधिक कुशल हो सकते हैं; यह संपूर्ण सारणियों को आबंटित और कॉपी करने के बजाय केवल संकेत और आकार स्वैप करता है। कार्यक्षमता और दक्षता में इस बोनस के अलावा, हम अब कॉपी-एंड-स्वैप मुहावरे को लागू करने के लिए तैयार हैं।
आगे की हलचल के बिना, हमारा असाइनमेंट ऑपरेटर है:
dumb_array& operator=(dumb_array other) // (1)
{
swap(*this, other); // (2)
return *this;
}
और बस! एक झपट्टा मारने के साथ, सभी तीन समस्याओं को एक साथ सुरुचिपूर्ण ढंग से निपटाया जाता है।
यह काम क्यों करता है?
हम पहले एक महत्वपूर्ण विकल्प नोटिस करते हैं: पैरामीटर तर्क को मान द्वारा लिया जाता है । जबकि एक बस के रूप में आसानी से निम्नलिखित कर सकते हैं (और वास्तव में, मुहावरे के कई अनुभवहीन कार्यान्वयन):
dumb_array& operator=(const dumb_array& other)
{
dumb_array temp(other);
swap(*this, temp);
return *this;
}
हम एक महत्वपूर्ण अनुकूलन अवसर खो देते हैं । इतना ही नहीं, लेकिन यह पसंद C ++ 11 में महत्वपूर्ण है, जिसकी चर्चा बाद में की गई है। (सामान्य नोट पर, उल्लेखनीय रूप से उपयोगी दिशानिर्देश इस प्रकार है: यदि आप किसी फ़ंक्शन में किसी चीज़ की प्रतिलिपि बनाने जा रहे हैं, तो कंपाइलर इसे पैरामीटर सूची में दें।,)
किसी भी तरह से, हमारे संसाधन प्राप्त करने का यह तरीका कोड डुप्लीकेशन को समाप्त करने की कुंजी है: हमें कॉपी बनाने के लिए कॉपी-कंस्ट्रक्टर से कोड का उपयोग करने के लिए मिलता है, और इसे किसी भी बिट को दोहराने की आवश्यकता नहीं है। अब जब कॉपी तैयार हो गई है, तो हम स्वैप करने के लिए तैयार हैं।
फ़ंक्शन में प्रवेश करने पर ध्यान दें कि सभी नए डेटा पहले से ही आवंटित, कॉपी और उपयोग किए जाने के लिए तैयार हैं। यह वही है जो हमें मुफ्त में एक मजबूत अपवाद की गारंटी देता है: यदि प्रतिलिपि का निर्माण विफल रहता है, तो हम फ़ंक्शन में भी प्रवेश नहीं करेंगे, और इसलिए इसकी स्थिति को बदलना संभव नहीं है *this
। (मजबूत अपवाद गारंटी के लिए हमने मैन्युअल रूप से पहले क्या किया था, कंपाइलर अब हमारे लिए क्या कर रहा है? कैसा लगा।)
इस बिंदु पर हम घर-मुक्त हैं, क्योंकि swap
गैर-फेंकना है। हम अपने वर्तमान डेटा को कॉपी किए गए डेटा के साथ स्वैप करते हैं, हमारे राज्य को सुरक्षित रूप से बदल देते हैं, और पुराना डेटा अस्थायी में डाल दिया जाता है। पुराने डेटा तब रिलीज़ होता है जब फ़ंक्शन वापस आता है। (जहां पैरामीटर्स का दायरा समाप्त हो जाता है और इसके विध्वंसक को कहा जाता है।)
क्योंकि मुहावरा कोई कोड नहीं दोहराता है, हम ऑपरेटर के भीतर बग का परिचय नहीं दे सकते हैं। ध्यान दें कि इसका मतलब है कि हम एक समान कार्यान्वयन की अनुमति देते हुए, स्व-असाइनमेंट चेक की आवश्यकता से मुक्त हैं operator=
। (इसके अतिरिक्त, अब हमारे पास गैर-स्व-असाइनमेंट पर प्रदर्शन जुर्माना नहीं है।)
और वह है नकल-और-अदला-बदली।
C ++ 11 के बारे में क्या?
C ++, C ++ 11 का अगला संस्करण, हम संसाधनों का प्रबंधन करने के तरीके में एक बहुत महत्वपूर्ण बदलाव करते हैं: तीन का नियम अब चार का नियम (और एक आधा) है। क्यों? क्योंकि न केवल हमें अपने संसाधन की प्रतिलिपि बनाने में सक्षम होने की आवश्यकता है, बल्कि हमें इसे भी स्थानांतरित करने की आवश्यकता है ।
हमारे लिए सौभाग्य से, यह आसान है:
class dumb_array
{
public:
// ...
// move constructor
dumb_array(dumb_array&& other) noexcept ††
: dumb_array() // initialize via default constructor, C++11 only
{
swap(*this, other);
}
// ...
};
यहाँ क्या चल रहा है? चाल-निर्माण के लक्ष्य को याद करें: संसाधनों को कक्षा के किसी अन्य उदाहरण से लेने के लिए, उसे राज्य में छोड़ने के लिए असाइन करने योग्य और विनाशकारी होने की गारंटी।
तो जो हमने किया है वह सरल है: डिफ़ॉल्ट कंस्ट्रक्टर (एक C ++ 11 सुविधा) के माध्यम से आरंभ करें, फिर स्वैप करें other
; हम जानते हैं कि हमारी कक्षा का एक डिफ़ॉल्ट निर्मित उदाहरण सुरक्षित रूप से सौंपा और नष्ट किया जा सकता है, इसलिए हमें पता है कि other
स्वैप करने के बाद भी ऐसा करने में सक्षम होगा।
(ध्यान दें कि कुछ संकलक कंस्ट्रक्टर के प्रतिनिधिमंडल का समर्थन नहीं करते हैं; इस मामले में, हमें मैन्युअल रूप से क्लास का निर्माण करना होगा। यह एक दुर्भाग्यपूर्ण लेकिन सौभाग्यशाली तुच्छ कार्य है।)
वह काम क्यों करता है?
वह एकमात्र परिवर्तन है जिसे हमें अपनी कक्षा में करने की आवश्यकता है, इसलिए यह काम क्यों करता है? पैरामीटर को एक मान बनाने के लिए किए गए कभी-महत्वपूर्ण निर्णय को याद रखें और संदर्भ नहीं:
dumb_array& operator=(dumb_array other); // (1)
अब, यदि other
एक प्रतिद्वंद्विता के साथ आरंभ किया जा रहा है , तो इसे स्थानांतरित किया जाएगा । उत्तम। उसी तरह C ++ 03 हमें तर्क-मान द्वारा हमारी कॉपी-कंस्ट्रक्टर कार्यक्षमता का फिर से उपयोग करने दें, C ++ 11 स्वचालित रूप से जब भी उचित हो, मूव-कंस्ट्रक्टर को चुन लेगा । (और, ज़ाहिर है, जैसा कि पहले लिंक किए गए लेख में बताया गया है, मूल्य की नकल / चलती बस पूरी तरह से बढ़ाई जा सकती है।)
और इसलिए कॉपी-एंड-स्वैप मुहावरा समाप्त होता है।
फुटनोट
* हम mArray
अशक्त क्यों होते हैं ? क्योंकि यदि ऑपरेटर में कोई और कोड फेंकता है, तो विध्वंसक को dumb_array
बुलाया जा सकता है; और अगर ऐसा होता है तो इसे बिना किसी सेटिंग के, हम उस मेमोरी को डिलीट करने का प्रयास करते हैं जो पहले ही डिलीट हो चुकी है! हम इसे शून्य पर सेट करने से बचते हैं, क्योंकि नल हटाना एक बिना ऑपरेशन है।
† अन्य दावे हैं कि हमें std::swap
अपने प्रकार के लिए विशेषज्ञ होना चाहिए , एक इन-क्लास swap
साथ-साथ एक मुफ्त-फ़ंक्शन प्रदान करना चाहिए swap
, लेकिन यह सब अनावश्यक है: किसी भी तरह का उचित उपयोग swap
एक अयोग्य कॉल के माध्यम से होगा, और हमारा कार्य होगा ADL के माध्यम से मिला । एक फंक्शन करेंगे।
To इसका कारण सरल है: एक बार आपके पास संसाधन होने के बाद, आप इसे स्वैप कर सकते हैं और / या इसे स्थानांतरित कर सकते हैं (C ++ 11) कहीं भी इसे होना चाहिए। और पैरामीटर सूची में प्रतिलिपि बनाकर, आप अधिकतम अनुकूलन करते हैं।
Code मूव कंस्ट्रक्टर को आम तौर पर होना चाहिए noexcept
, अन्यथा कुछ कोड (जैसे std::vector
तर्क का आकार बदलना) कॉपी कंस्ट्रक्टर का उपयोग तब भी करेगा जब कोई चाल समझ में आएगी। यदि कोड अंदर अपवादों को नहीं फेंकता है, तो निश्चित रूप से इसे केवल noexcept चिह्नित करें।