असाइनमेंट ऑपरेटर और `if (यह! = & Rhs)` को स्थानांतरित करें


126

एक वर्ग के असाइनमेंट ऑपरेटर में, आपको आमतौर पर यह जांचने की आवश्यकता होती है कि ऑब्जेक्ट असाइन किया जा रहा है या नहीं, तो आप इसे खराब नहीं करेंगे।

Class& Class::operator=(const Class& rhs) {
    if (this != &rhs) {
        // do the assignment
    }

    return *this;
}

क्या आपको मूव असाइनमेंट ऑपरेटर के लिए समान चीज़ की आवश्यकता है? क्या कभी ऐसी स्थिति है जहां this == &rhsसच होगा?

? Class::operator=(Class&& rhs) {
    ?
}

12
क्यू के लिए अप्रासंगिक, और सिर्फ इतना है कि नए उपयोगकर्ताओं को जो इस क्यू नीचे समयरेखा पढ़ें (क्योंकि मुझे पता है सेठ पहले से ही यह जानता है) गलत विचार नहीं है, कॉपी और स्वैप कॉपी असाइनमेंट ऑपरेटर को लागू करने का सही तरीका है जिसमें आप सेल्फ असाइनमेंट एट-ऑल की जांच करने की जरूरत नहीं है।
आलोक सेव

5
@VaughnCato: A a; a = std::move(a);
Xeo

11
@VaughnCato का उपयोग करना std::moveसामान्य है। फिर खाते को ध्यान में रखें, और जब आप कॉल स्टैक के अंदर गहरे होते हैं और आपके पास एक संदर्भ होता है T, और एक अन्य संदर्भ T... क्या आप पहचान के लिए यहां जा रहे हैं? क्या आप पहले कॉल (या कॉल) को ढूंढना चाहते हैं, जहां दस्तावेज है कि आप एक ही तर्क को दो बार पारित नहीं कर सकते हैं, यह साबित होगा कि उन दो संदर्भों को अन्य नाम नहीं दिया जाएगा? या आप सिर्फ काम करने के लिए आत्म-असाइनमेंट करेंगे?
ल्यूक डैंटन

2
@ ल्यूकैंटन मैं असाइनमेंट ऑपरेटर में दावा करना चाहता हूं। अगर std :: Move का उपयोग इस तरह से किया जाता था कि यह एक स्व-असाइनमेंट के साथ समाप्त होना संभव था, तो मैं इसे एक बग मानूंगा जिसे ठीक किया जाना चाहिए।
वॉन काटो

4
@VaughnCato एक जगह है कि स्व-स्वैप सामान्य है std::sortया तो अंदर है std::shuffle- या किसी भी समय आप पहले की जाँच के बिना एक सरणी के iवें और jवें तत्वों को स्वैप कर रहे हैं i != j। ( std::swapचाल असाइनमेंट के संदर्भ में कार्यान्वित किया जाता है।)
क्क्सप्लसोन

जवाबों:


143

वाह, यहाँ साफ करने के लिए बस इतना है ...

सबसे पहले, कॉपी और स्वैप हमेशा कॉपी असाइनमेंट को लागू करने का सही तरीका नहीं है। लगभग निश्चित रूप से dumb_array, इस मामले में , यह एक उप-इष्टतम समाधान है।

कॉपी और स्वैप का उपयोग dumb_arrayनिम्न स्तर पर पूर्ण सुविधाओं के साथ सबसे महंगा ऑपरेशन डालने का एक उत्कृष्ट उदाहरण है। यह उन ग्राहकों के लिए एकदम सही है जो पूर्ण सुविधा चाहते हैं और प्रदर्शन दंड का भुगतान करने के लिए तैयार हैं। उन्हें वही मिलता है जो वे चाहते हैं।

लेकिन यह उन ग्राहकों के लिए विनाशकारी है जिन्हें पूर्ण सुविधा की आवश्यकता नहीं है और वे उच्चतम प्रदर्शन की तलाश कर रहे हैं। उनके लिए dumb_arrayसॉफ्टवेयर का सिर्फ एक और टुकड़ा है जो उन्हें फिर से लिखना होगा क्योंकि यह बहुत धीमा है। dumb_arrayअलग तरह से डिजाइन किया गया था , यह दोनों ग्राहकों को संतुष्ट कर सकता है जिसमें कोई भी समझौता नहीं है।

दोनों ग्राहकों को संतुष्ट करने की कुंजी सबसे निचले स्तर पर सबसे तेज संचालन का निर्माण करना है, और फिर अधिक खर्च पर फुलर सुविधाओं के लिए इसके ऊपर एपीआई जोड़ना है। यानी आपको मजबूत अपवाद गारंटी की आवश्यकता है, ठीक है, आप इसके लिए भुगतान करते हैं। आपको इसकी आवश्यकता नहीं है? यहाँ एक तेज़ उपाय है।

चलो ठोस हो जाओ: यहाँ के लिए तेज, बुनियादी अपवाद गारंटी कॉपी असाइनमेंट ऑपरेटर है dumb_array:

dumb_array& operator=(const dumb_array& other)
{
    if (this != &other)
    {
        if (mSize != other.mSize)
        {
            delete [] mArray;
            mArray = nullptr;
            mArray = other.mSize ? new int[other.mSize] : nullptr;
            mSize = other.mSize;
        }
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}

स्पष्टीकरण:

आधुनिक हार्डवेयर पर आप कर सकते हैं और अधिक महंगी चीजों में से एक ढेर के लिए एक यात्रा है। कुछ भी तुम ढेर के लिए एक यात्रा से बचने के लिए कर सकते हैं समय और प्रयास अच्छी तरह से बिताया है। ग्राहक dumb_arrayअच्छी तरह से एक ही आकार के सरणियों को असाइन करना चाहते हैं। और जब वे करते हैं, तो आपको केवल एक memcpy(छिपा हुआ std::copy) होना चाहिए। आप एक ही आकार के एक नए सरणी को आवंटित नहीं करना चाहते हैं और फिर उसी आकार के पुराने को हटा दें!

अब आपके ग्राहकों के लिए जो वास्तव में मजबूत अपवाद सुरक्षा चाहते हैं:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    swap(lhs, rhs);
    return lhs;
}

या हो सकता है कि अगर आप C ++ 11 में मूव असाइनमेंट का लाभ उठाना चाहते हैं जो कि होना चाहिए:

template <class C>
C&
strong_assign(C& lhs, C rhs)
{
    lhs = std::move(rhs);
    return lhs;
}

यदि dumb_arrayग्राहकों की गति की कीमत है, तो उन्हें फोन करना चाहिए operator=। यदि उन्हें मजबूत अपवाद सुरक्षा की आवश्यकता है, तो जेनेरिक एल्गोरिदम हैं जो वे कॉल कर सकते हैं जो विभिन्न प्रकार की वस्तुओं पर काम करेंगे और केवल एक बार लागू होने की आवश्यकता होगी।

अब वापस मूल प्रश्न पर (जो इस समय एक टाइप-ओ है):

Class&
Class::operator=(Class&& rhs)
{
    if (this == &rhs)  // is this check needed?
    {
       // ...
    }
    return *this;
}

यह वास्तव में एक विवादास्पद प्रश्न है। कुछ कहेंगे हाँ, बिल्कुल, कुछ कहेंगे नहीं।

मेरी व्यक्तिगत राय नहीं है, आपको इस चेक की आवश्यकता नहीं है।

दलील:

जब कोई वस्तु एक रेवल्यू रेफरेंस से जुड़ती है तो यह दो चीजों में से एक है:

  1. अस्थायी।
  2. कॉल करने वाला एक ऑब्जेक्ट चाहता है कि आप विश्वास करें कि वह अस्थायी है।

यदि आपके पास किसी ऐसी वस्तु का संदर्भ है जो वास्तविक अस्थायी है, तो परिभाषा के अनुसार, आपके पास उस वस्तु का एक अनूठा संदर्भ है। यह संभवतः आपके पूरे कार्यक्रम में कहीं और संदर्भित नहीं किया जा सकता है। Ie this == &temporary संभव नहीं है

अब यदि आपके ग्राहक ने आपसे झूठ बोला है और आपसे वादा किया है कि जब आप नहीं होते हैं तो आप एक अस्थायी हो जाते हैं, तो यह ग्राहक की जिम्मेदारी है कि आप सुनिश्चित करें कि आपको परवाह नहीं है। यदि आप वास्तव में सावधान रहना चाहते हैं, तो मेरा मानना ​​है कि यह एक बेहतर कार्यान्वयन होगा:

Class&
Class::operator=(Class&& other)
{
    assert(this != &other);
    // ...
    return *this;
}

यानी यदि आप कर रहे हैं एक आत्म संदर्भ पारित कर दिया, इस ग्राहक कि समाधान हो गया होगा की ओर से एक बग है।

पूर्णता के लिए, यहाँ एक चाल असाइनमेंट ऑपरेटर है dumb_array:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

चाल असाइनमेंट के विशिष्ट उपयोग के मामले में, *thisएक स्थानांतरित-से-ऑब्जेक्ट होगा और इसलिए delete [] mArray;एक नो-ऑप होना चाहिए। यह महत्वपूर्ण है कि कार्यान्वयन संभव के रूप में उपवास पर एक nullptr पर हटा दें।

चेतावनी:

कुछ लोग तर्क देंगे कि swap(x, x)यह एक अच्छा विचार है, या सिर्फ एक आवश्यक बुराई है। और यह, यदि स्वैप डिफ़ॉल्ट स्वैप पर जाता है, तो स्व-चाल-असाइनमेंट का कारण बन सकता है।

मैं इससे सहमत नहीं है कि swap(x, x)है कभी एक अच्छा विचार। यदि मेरे अपने कोड में पाया जाता है, तो मैं इसे एक प्रदर्शन बग पर विचार करूंगा और इसे ठीक करूंगा। लेकिन अगर आप इसे अनुमति देना चाहते हैं, तो महसूस करें कि swap(x, x)केवल मूव-ऑन मूल्य पर सेल्फ-मूव-असाइनमेनेट करता है। और हमारे dumb_arrayउदाहरण में यह पूरी तरह से हानिरहित होगा यदि हम केवल मुखर को छोड़ देते हैं, या इसे स्थानांतरित मामले से विवश करते हैं:

dumb_array& operator=(dumb_array&& other)
{
    assert(this != &other || mSize == 0);
    delete [] mArray;
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

यदि आप दो स्थानांतरित-से (खाली) स्व-असाइन dumb_arrayकरते हैं, तो आप अपने प्रोग्राम में बेकार निर्देश डालने से अलग कुछ भी गलत नहीं करते हैं। यह एक ही अवलोकन वस्तुओं के विशाल बहुमत के लिए बनाया जा सकता है।

<अपडेट करें>

मैंने इस मुद्दे को कुछ और सोचा है, और अपनी स्थिति को कुछ हद तक बदल दिया है। अब मेरा मानना ​​है कि असाइनमेंट सेल्फ असाइनमेंट के प्रति सहनशील होना चाहिए, लेकिन यह कि कॉपी असाइनमेंट और मूव असाइनमेंट पर पोस्ट की शर्तें अलग-अलग हैं:

कॉपी असाइनमेंट के लिए:

x = y;

एक के बाद की स्थिति yहोनी चाहिए कि मूल्य में परिवर्तन नहीं होना चाहिए। जब &x == &yयह पोस्टकंडिशन में अनुवाद होता है: सेल्फ कॉपी असाइनमेंट का मूल्य पर कोई प्रभाव नहीं होना चाहिए x

चाल असाइनमेंट के लिए:

x = std::move(y);

एक के पास एक ऐसी स्थिति होनी चाहिए, yजिसकी वैध लेकिन अनिर्दिष्ट स्थिति हो। जब &x == &yयह पोस्टकंडिशन में तब्दील हो जाता है: xएक मान्य लेकिन अनिर्दिष्ट राज्य है। Ie सेल्फ मूव असाइनमेंट का नो-ऑप होना जरूरी है। लेकिन यह दुर्घटना नहीं होनी चाहिए। यह swap(x, x)काम करने की स्थिति के अनुरूप है :

template <class T>
void
swap(T& x, T& y)
{
    // assume &x == &y
    T tmp(std::move(x));
    // x and y now have a valid but unspecified state
    x = std::move(y);
    // x and y still have a valid but unspecified state
    y = std::move(tmp);
    // x and y have the value of tmp, which is the value they had on entry
}

ऊपर काम करता है, जब तक x = std::move(x)दुर्घटना नहीं होती है। यह xकिसी भी वैध लेकिन अनिर्दिष्ट स्थिति में छोड़ सकता है ।

मैं dumb_arrayइसे प्राप्त करने के लिए मूव असाइनमेंट ऑपरेटर को प्रोग्राम करने के तीन तरीके देखता हूं :

dumb_array& operator=(dumb_array&& other)
{
    delete [] mArray;
    // set *this to a valid state before continuing
    mSize = 0;
    mArray = nullptr;
    // *this is now in a valid state, continue with move assignment
    mSize = other.mSize;
    mArray = other.mArray;
    other.mSize = 0;
    other.mArray = nullptr;
    return *this;
}

उपरोक्त क्रियान्वयन बर्दाश्त स्वयं काम है, लेकिन *thisऔर otherआत्म-चाल काम के बाद एक शून्य आकार सरणी, कोई बात नहीं क्या की मूल मूल्य जा रहा है खत्म हो *thisगया है। यह ठीक है।

dumb_array& operator=(dumb_array&& other)
{
    if (this != &other)
    {
        delete [] mArray;
        mSize = other.mSize;
        mArray = other.mArray;
        other.mSize = 0;
        other.mArray = nullptr;
    }
    return *this;
}

उपरोक्त कार्यान्वयन स्वयं असाइनमेंट को उसी तरह से सहन करता है जिस तरह से कॉपी असाइनमेंट ऑपरेटर करता है, इसे नो-ऑप बनाकर। यह भी ठीक है।

dumb_array& operator=(dumb_array&& other)
{
    swap(other);
    return *this;
}

उपरोक्त केवल तभी ठीक है जब dumb_arrayसंसाधनों को पकड़ न लिया जाए जो "तुरंत" नष्ट हो जाना चाहिए। उदाहरण के लिए यदि एकमात्र संसाधन मेमोरी है, तो उपरोक्त ठीक है। यदि dumb_arrayसंभवत: म्यूटेक्स लॉक या फ़ाइलों की खुली स्थिति को पकड़ सकता है, तो क्लाइंट यथोचित रूप से उन संसाधनों की अपेक्षा कर सकता है जो तुरंत जारी किए जाने वाले कदम असाइनमेंट के एलएचएस पर हैं और इसलिए यह कार्यान्वयन समस्याग्रस्त हो सकता है।

पहले की लागत दो अतिरिक्त स्टोर हैं। दूसरे की लागत एक परीक्षण और शाखा है। दोनों कार्य। दोनों C ++ 11 मानक में तालिका 22 MoveAssignable आवश्यकताओं की सभी आवश्यकताओं को पूरा करते हैं। तीसरा गैर-मेमोरी-रिसोर्स-चिंता का काम भी करता है।

सभी तीन कार्यान्वयन हार्डवेयर के आधार पर अलग-अलग लागत हो सकते हैं: एक शाखा कितनी महंगी है? क्या बहुत सारे रजिस्टर या बहुत कम हैं?

टेक-ऑफ यह है कि स्व-चाल-असाइनमेंट, स्व-कॉपी-असाइनमेंट के विपरीत, वर्तमान मूल्य को संरक्षित करने के लिए नहीं है।

</अपडेट करें>

एक अंतिम (उम्मीद है) ल्यूक डेंटन की टिप्पणी से प्रेरित संपादित करें:

यदि आप एक उच्च स्तरीय वर्ग लिख रहे हैं जो सीधे मेमोरी का प्रबंधन नहीं करता है (लेकिन आधार या सदस्य हो सकते हैं), तो चाल असाइनमेंट का सबसे अच्छा कार्यान्वयन अक्सर होता है:

Class& operator=(Class&&) = default;

यह प्रत्येक बेस और प्रत्येक सदस्य को बदले में असाइन करेगा, और इसमें एक this != &otherचेक शामिल नहीं होगा । यह आपको सबसे उच्च प्रदर्शन और बुनियादी अपवाद सुरक्षा देगा, यह मानते हुए कि आपके ठिकानों और सदस्यों के बीच किसी भी प्रकार के आक्रमणकारियों को बनाए रखने की आवश्यकता नहीं है। अपने ग्राहकों को मजबूत अपवाद सुरक्षा की मांग के लिए, उन्हें इंगित करें strong_assign


6
मुझे नहीं पता कि इस उत्तर के बारे में कैसा महसूस होता है। यह ऐसी कक्षाओं को लागू करने जैसा दिखता है (जो उनकी स्मृति को बहुत स्पष्ट रूप से प्रबंधित करता है) ऐसा करना एक सामान्य बात है। यह सच है कि जब आप ऐसी कक्षा लिखते हैं तो अपवाद सुरक्षा गारंटी के बारे में बहुत सावधान रहना पड़ता है और इंटरफ़ेस के लिए मीठा स्थान ढूंढना संक्षिप्त और सुविधाजनक है, लेकिन सवाल सामान्य सलाह के लिए पूछ रहा है।
ल्यूक डैंटन

हाँ, मैं निश्चित रूप से कभी भी कॉपी-एंड-स्वैप का उपयोग नहीं करता क्योंकि यह उन कक्षाओं के लिए समय की बर्बादी है जो संसाधनों और चीजों का प्रबंधन करते हैं (क्यों आपके सभी डेटा की एक और पूरी प्रतिलिपि बनाते हैं?)। और धन्यवाद, यह मेरे सवाल का जवाब देता है।
सेठ कार्नेगी

5
सुझाव है कि चाल-असाइनमेंट-से-स्वयं के लिए डाउनवॉट किया जाना चाहिए कभी असफल हो या एक "अनिर्दिष्ट" परिणाम का उत्पादन। असाइनमेंट-से-स्व का शाब्दिक अधिकार प्राप्त करने का सबसे आसान मामला है। यदि आपकी कक्षा दुर्घटनाग्रस्त हो जाती है std::swap(x,x), तो मुझे अधिक जटिल परिचालनों को सही ढंग से संभालने के लिए उस पर भरोसा क्यों करना चाहिए?
क्क्सप्लसोन

1
@Quuxplusone: मैं आपके साथ मुखर होने पर सहमत होने के लिए आया हूं, जैसा कि मेरे जवाब के अपडेट में दिया गया है। जहाँ तक std::swap(x,x)जाता है, यह तब भी काम करता है जब x = std::move(x)अनिर्दिष्ट परिणाम उत्पन्न करता है। कोशिश करो! आपको मुझ पर विश्वास नहीं करना है।
हावर्ड हिनेंट

@ हॉवर्डहिनेंट अच्छा बिंदु, किसी भी चाल-चल सकने की स्थिति में पत्तियों के swapरूप में लंबे समय तक काम करता है । और / एल्गोरिदम को परिभाषित किया गया है ताकि पहले से ही नो-ऑप प्रतियों पर अपरिभाषित व्यवहार का उत्पादन किया जा सके (ouch; 20-वर्षीय पुराने को तुच्छ मामला सही मिलता है लेकिन नहीं!)। इसलिए मुझे लगता है कि मैंने अभी तक स्व-असाइनमेंट के लिए "स्लैम डंक" के बारे में नहीं सोचा है। लेकिन स्पष्ट रूप से स्व-असाइनमेंट कुछ ऐसा है जो वास्तविक कोड में बहुत कुछ होता है, चाहे मानक ने इसे आशीर्वाद दिया हो या नहीं। x = move(x)xstd::copystd::movememmovestd::move
क्क्सप्लसोन

11

पहले, आपको मूव-असाइनमेंट ऑपरेटर के हस्ताक्षर गलत मिले। चूंकि स्रोत ऑब्जेक्ट से चोरी के संसाधन चलते हैं, इसलिए स्रोत को गैर const-आर-मूल्य संदर्भ होना चाहिए ।

Class &Class::operator=( Class &&rhs ) {
    //...
    return *this;
}

ध्यान दें कि आप अभी भी एक (गैर- const) l -value संदर्भ के माध्यम से लौटते हैं ।

प्रत्यक्ष असाइनमेंट के किसी भी प्रकार के लिए, मानक स्व-असाइनमेंट की जांच करने के लिए नहीं है, लेकिन यह सुनिश्चित करने के लिए कि स्व-असाइनमेंट क्रैश-एंड-बर्न का कारण नहीं बनता है। आम तौर पर, कोई भी स्पष्ट रूप से करता है x = xया y = std::move(y)कॉल, लेकिन उपघटन विशेष रूप से कई कार्यों के माध्यम से, जन्म दे सकती है a = bया c = std::move(d)किया जा रहा है आत्म-कार्य में। स्व-असाइनमेंट के लिए एक स्पष्ट जांच, अर्थात this == &rhs, फ़ंक्शन का मांस स्केप करता है, जब सत्य स्वयं-असाइनमेंट सुरक्षा सुनिश्चित करने का एक तरीका है। लेकिन यह सबसे खराब तरीकों में से एक है, क्योंकि यह एक (उम्मीद) दुर्लभ मामले का अनुकूलन करता है जबकि यह अधिक सामान्य मामले के लिए एक विरोधी अनुकूलन है (शाखा और संभवतः कैश मिस होने के कारण)।

अब जब (कम से कम) ऑपरेंड में से एक सीधे अस्थायी ऑब्जेक्ट है, तो आपके पास कभी भी स्व-असाइनमेंट परिदृश्य नहीं हो सकता है। कुछ लोग उस मामले को संभालने की वकालत करते हैं और इसके लिए कोड को इतना अनुकूलित करते हैं कि यह धारणा गलत होने पर कोड आत्मघाती हो जाता है। मैं कहता हूं कि यूजर्स पर समान ऑब्जेक्ट चेक डंप करना गैर जिम्मेदाराना है। हम कॉपी-असाइनमेंट के लिए यह तर्क नहीं देते हैं; चाल-असाइनमेंट के लिए स्थिति को उल्टा क्यों करें?

चलो एक उदाहरण बनाते हैं, एक और प्रतिवादी से बदल दिया गया है:

dumb_array& dumb_array::operator=(const dumb_array& other)
{
    if (mSize != other.mSize)
    {
        delete [] mArray;
        mArray = nullptr;  // clear this...
        mSize = 0u;        // ...and this in case the next line throws
        mArray = other.mSize ? new int[other.mSize] : nullptr;
        mSize = other.mSize;
    }
    std::copy(other.mArray, other.mArray + mSize, mArray);
    return *this;
}

यह कॉपी-असाइनमेंट एक स्पष्ट जाँच के बिना स्व-असाइनमेंट को इनायत से संभालता है। यदि स्रोत और गंतव्य का आकार अलग-अलग है, तो डीलक्लोकेशन और रीएलोकेशन नकल से पहले आता है। अन्यथा, सिर्फ नकल की जाती है। स्व-असाइनमेंट को एक अनुकूलित पथ नहीं मिलता है, यह उसी पथ में डंप हो जाता है जब स्रोत और गंतव्य आकार बराबर शुरू होते हैं। जब दोनों वस्तुएं समान हों (जब वे एक ही वस्तु हों) सहित, तब तक नकल तकनीकी रूप से अनावश्यक है, लेकिन जब एक चेक (वैल्यू-वार या एड्रेस-वाइज) नहीं कर रहा हो तो इसकी कीमत बताई जाती है क्योंकि चेक स्वयं ही सबसे अधिक बेकार होगा। समय की। ध्यान दें कि यहाँ वस्तु-स्व-असाइनमेंट तत्व-स्तरीय स्व-असाइनमेंट की एक श्रृंखला का कारण होगा; ऐसा करने के लिए तत्व प्रकार सुरक्षित होना चाहिए।

अपने स्रोत उदाहरण की तरह, यह कॉपी-असाइनमेंट बुनियादी अपवाद सुरक्षा गारंटी प्रदान करता है। यदि आप मजबूत गारंटी चाहते हैं, तो मूल प्रतिलिपि और स्वैप क्वेरी से एकीकृत-असाइनमेंट ऑपरेटर का उपयोग करें , जो प्रतिलिपि- और स्थानांतरित-असाइनमेंट दोनों को संभालता है। लेकिन इस उदाहरण का उद्देश्य गति प्राप्त करने के लिए सुरक्षा को एक रैंक से कम करना है। (बीटीडब्लू, हम मान रहे हैं कि व्यक्तिगत तत्वों के मूल्य स्वतंत्र हैं; दूसरों की तुलना में कुछ अपरिवर्तनीय बाधाओं को सीमित करने का कोई कारण नहीं है।)

आइए इस प्रकार के लिए एक चाल-असाइनमेंट देखें:

class dumb_array
{
    //...
    void swap(dumb_array& other) noexcept
    {
        // Just in case we add UDT members later
        using std::swap;

        // both members are built-in types -> never throw
        swap( this->mArray, other.mArray );
        swap( this->mSize, other.mSize );
    }

    dumb_array& operator=(dumb_array&& other) noexcept
    {
        this->swap( other );
        return *this;
    }
    //...
};

void  swap( dumb_array &l, dumb_array &r ) noexcept  { l.swap( r ); }

एक स्वैपेबल प्रकार जिसे अनुकूलन की आवश्यकता होती है, के पास एक दो-तर्क मुक्त फ़ंक्शन होना चाहिए जिसे swapउसी नामस्थान में टाइप के रूप में कहा जाता है। (नाम स्थान प्रतिबंध काम करने के लिए अयोग्य कॉल करने की अनुमति देता है।) एक कंटेनर प्रकार को swapमानक कंटेनरों से मिलान करने के लिए एक सार्वजनिक सदस्य फ़ंक्शन भी जोड़ना चाहिए । यदि कोई सदस्य swapप्रदान नहीं किया गया है, तो मुफ्त-फ़ंक्शन को swapसंभवतः स्वैपेबल प्रकार के मित्र के रूप में चिह्नित करने की आवश्यकता है। यदि आप उपयोग करने के लिए चालों को अनुकूलित करते हैं swap, तो आपको अपना स्वयं का स्वैपिंग कोड प्रदान करना होगा; मानक कोड प्रकार के चाल कोड को कॉल करता है, जिसके परिणामस्वरूप स्थानांतरित-अनुकूलित प्रकारों के लिए अनंत पारस्परिक पुनरावृत्ति होगी।

विध्वंसक की तरह, स्वैप फ़ंक्शन और मूव ऑपरेशंस को कभी भी संभव नहीं होने पर फेंक दिया जाना चाहिए, और संभवत: ऐसे (C ++ 11 में) चिह्नित किया जाए। मानक पुस्तकालय प्रकार और दिनचर्या में गैर-फेंकने योग्य प्रकारों के लिए अनुकूलन होते हैं।

मूव-असाइनमेंट का यह पहला संस्करण मूल अनुबंध को पूरा करता है। स्रोत के संसाधन मार्करों को गंतव्य ऑब्जेक्ट पर स्थानांतरित किया जाता है। पुराने संसाधन लीक नहीं होंगे क्योंकि स्रोत ऑब्जेक्ट अब उन्हें प्रबंधित करता है। और स्रोत वस्तु को एक प्रयोग करने योग्य स्थिति में छोड़ दिया जाता है जहां असाइनमेंट और विनाश सहित आगे के संचालन को इसके लिए लागू किया जा सकता है।

ध्यान दें कि यह चाल-असाइनमेंट स्वचालित रूप से स्व-असाइनमेंट के लिए सुरक्षित है, क्योंकि swapकॉल है। यह भी दृढ़ता से अपवाद सुरक्षित है। समस्या अनावश्यक संसाधन प्रतिधारण है। गंतव्य के लिए पुराने संसाधनों की वैचारिक जरूरत नहीं रह गई है, लेकिन यहां वे अभी भी आस-पास ही हैं ताकि स्रोत वस्तु वैध रह सके। यदि स्रोत ऑब्जेक्ट का अनुसूचित विनाश एक लंबा रास्ता बंद है, तो हम संसाधन स्थान को बर्बाद कर रहे हैं, या इससे भी बदतर अगर कुल संसाधन स्थान सीमित है और अन्य संसाधन याचिकाएं (नए) स्रोत ऑब्जेक्ट आधिकारिक रूप से मर जाती हैं।

यह मुद्दा वही है जिसके कारण विवादास्पद वर्तमान गुरु सलाह चलती-असाइनमेंट के दौरान स्व-लक्ष्यीकरण से संबंधित है। संसाधनों के बिना चाल-चलन लिखने का तरीका कुछ इस प्रकार है:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        delete [] this->mArray;  // kill old resources
        this->mArray = other.mArray;
        this->mSize = other.mSize;
        other.mArray = nullptr;  // reset source
        other.mSize = 0u;
        return *this;
    }
    //...
};

स्रोत डिफ़ॉल्ट स्थितियों पर रीसेट हो जाता है, जबकि पुराने गंतव्य संसाधन नष्ट हो जाते हैं। स्व-असाइनमेंट मामले में, आपकी वर्तमान वस्तु आत्महत्या कर लेती है। इसके आस-पास का मुख्य तरीका एक if(this != &other)ब्लॉक के साथ एक्शन कोड को घेरना है , या इसे स्क्रू करना है और ग्राहकों को एक assert(this != &other)प्रारंभिक पंक्ति (यदि आप अच्छा महसूस कर रहे हैं) खाने दें ।

एक विकल्प यह है कि कॉपी-असाइनमेंट को दृढ़ता से अपवाद के बिना, एकीकृत-असाइनमेंट के बिना सुरक्षित करने के लिए अध्ययन करें और इसे स्थानांतरित-असाइन करने के लिए लागू करें:

class dumb_array
{
    //...
    dumb_array& operator=(dumb_array&& other) noexcept
    {
        dumb_array  temp{ std::move(other) };

        this->swap( temp );
        return *this;
    }
    //...
};

जब otherऔर thisअलग होते हैं, otherतो इस तरह से स्थानांतरित किया जाता है tempऔर उस तरह से रहता है। तब thisअपने पुराने संसाधनों को खो देता है tempजबकि मूल रूप से संसाधनों को प्राप्त करना other। फिर पुराने संसाधनों की thisहत्या कब tempहोती है।

जब स्वयं काम होता है, की खाली करने otherके लिए tempखाली thisके रूप में अच्छी तरह से। फिर लक्ष्य वस्तु अपने संसाधनों को वापस ले जाती है जब स्वैप tempऔर thistempदावे की मृत्यु एक खाली वस्तु है, जो व्यावहारिक रूप से एक नो-ऑप होना चाहिए। this/ otherवस्तु अपने संसाधनों रहता है।

चाल-असाइनमेंट को कभी भी फेंक नहीं दिया जाना चाहिए जब तक कि चाल-निर्माण और स्वैपिंग भी न हो। स्व-असाइनमेंट के दौरान भी सुरक्षित होने की लागत कम-स्तरीय प्रकारों पर कुछ और निर्देश हैं, जिन्हें डीलक्लोनेशन कॉल द्वारा बाहर निकाला जाना चाहिए।


क्या आपको यह जांचने की आवश्यकता है कि deleteआपके कोड के दूसरे ब्लॉक में कॉल करने से पहले कोई मेमोरी आवंटित की गई थी या नहीं ?
user3728501

3
आपका दूसरा कोड नमूना, स्व-असाइनमेंट जांच के बिना कॉपी-असाइनमेंट ऑपरेटर, गलत है। std::copyस्रोत और गंतव्य पर्वतमाला ओवरलैप (मामले में जब वे मेल खाते हैं) सहित अपरिभाषित व्यवहार का कारण बनता है। C ++ 14 [alg.copy] / 3 देखें।
MM

6

मैं उन लोगों के शिविर में हूं जो स्व-असाइनमेंट सुरक्षित ऑपरेटर चाहते हैं, लेकिन कार्यान्वयन में स्व-असाइनमेंट चेक लिखना नहीं चाहते हैं operator=। और वास्तव में मैं बिल्कुल भी लागू नहीं करना चाहता operator=, मैं चाहता हूं कि डिफ़ॉल्ट व्यवहार 'सही बॉक्स से बाहर' काम करे। सबसे अच्छा विशेष सदस्य वे हैं जो मुफ्त में आते हैं।

कहा जा रहा है कि, मानक में मौजूद MoveAssignable आवश्यकताओं को निम्नानुसार वर्णित किया गया है (17.6.3.1 टेम्पलेट तर्क आवश्यकताओं से [उपयोगिता.arg.requirements], n3290):

एक्सप्रेशन रिटर्न प्रकार रिटर्न वैल्यू पोस्ट-कंडीशन
t = rv T & tt असाइनमेंट से पहले rv के मान के बराबर है

जहां प्लेसहोल्डर्स को इस प्रकार वर्णित किया गया है: " t[एक प्रकार का टी का मॉडिफाइड लैवल्यू है;" और " rvटी का एक प्रकार है"; ध्यान दें कि वे मानक लाइब्रेरी के टेम्प्लेट के तर्क के रूप में उपयोग किए जाने वाले प्रकारों पर रखी गई आवश्यकताएं हैं, लेकिन मानक I में कहीं और देख रहे हैं कि कदम असाइनमेंट पर हर आवश्यकता इस के समान है।

इसका मतलब है कि a = std::move(a)'सुरक्षित' होना चाहिए। यदि आपको एक पहचान परीक्षण की आवश्यकता है (उदाहरण के लिए this != &other), तो इसके लिए जाएं, अन्यथा आप अपनी वस्तुओं को डालने में भी सक्षम नहीं होंगे std::vector! (जब तक आप उन सदस्यों / प्रचालनों का उपयोग नहीं करते हैं, जिन्हें MoveAssignable की आवश्यकता होती है; लेकिन ऐसा कभी नहीं होता।) ध्यान दें कि पिछले उदाहरण के साथ a = std::move(a), तब this == &otherवास्तव में पकड़ होगी।


क्या आप बता सकते हैं कि a = std::move(a)काम न करने से क्लास कैसे नहीं चलेगी std::vector? उदाहरण?
पॉल जे। लुकास

std::vector<T>::eraseजब तक MoveAssignable है @ @ PaulJ.Lucas कॉलिंग की अनुमति नहीं Tहै। (एक तरफ IIRC के रूप में कुछ MoveAssignable आवश्यकताओं को C ++ 14 के बजाय MoveInsertable में आराम दिया गया था।)
Luc Danton

ठीक है, तो TMoveAssignable होना चाहिए, लेकिन erase()एक तत्व को स्वयं पर ले जाने पर निर्भर क्यों होगा ?
पॉल जे। लुकास

@ पॉलजे.लुकास उस सवाल का कोई संतोषजनक जवाब नहीं है। 'अनुबंध न तोड़ने' के लिए सब कुछ उबलता है।
ल्यूक डैंटन

2

जैसा कि आपका वर्तमान operator=फ़ंक्शन लिखा है, चूंकि आपने रवैल्यू-रेफ़रेंस तर्क दिया है const, तो कोई तरीका नहीं है जिससे आप पॉइंटर्स को "चोरी" कर सकें और आने वाले रेवल्यू रेफरेंस के मूल्यों को बदल सकें ... आप बस इसे बदल नहीं सकते हैं, आप इससे केवल पढ़ सकता था। मुझे केवल एक मुद्दा दिखाई देगा यदि आप deleteअपनी thisवस्तु में पॉइंटर्स इत्यादि को कॉल करना शुरू कर रहे थे , जैसे कि आप एक सामान्य लव्यू-रेफरेंस operator=मेथड में होंगे, लेकिन उस तरह के रैवल्यू-वर्जन के पॉइंट को हरा देता है ... अर्थात, यह होगा मूल रूप से const-lvalue operator=विधि के लिए समान रूप से छोड़े गए कार्यों को मूल रूप से करने के लिए, रैवल्यू संस्करण का उपयोग करने के लिए अनावश्यक प्रतीत होता है ।

अब अगर आपने operator=गैर const-रवैल्यू-रेफरेंस लेने के लिए अपने को परिभाषित किया है , तो एक ही तरीका है कि मैं एक चेक को देख सकता हूं, यदि आप thisएक फ़ंक्शन को ऑब्जेक्ट पास करते हैं, जो जानबूझकर एक अस्थायी संदर्भ के बजाय एक रेवल्यू संदर्भ लौटाता है।

उदाहरण के लिए, मान लें कि किसी ने एक operator+फ़ंक्शन लिखने की कोशिश की है , और वस्तु-प्रकार पर कुछ स्टैक किए गए अतिरिक्त ऑपरेशन के दौरान अतिरिक्त टेम्पोररी बनाए जाने से रोकने के लिए प्रतिद्वंद्वियों के संदर्भों और अंतराल संदर्भों के मिश्रण का उपयोग करें:

struct A; //defines operator=(A&& rhs) where it will "steal" the pointers
          //of rhs and set the original pointers of rhs to NULL

A&& operator+(A& rhs, A&& lhs)
{
    //...code

    return std::move(rhs);
}

A&& operator+(A&& rhs, A&&lhs)
{
    //...code

    return std::move(rhs);
}

int main()
{
    A a;

    a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a

    //...rest of code
}

अब, मैं रेवल्यू रेफरेंस के बारे में जो समझ रहा हूं, उससे ऊपर हतोत्साहित करना (यानी, आपको सिर्फ एक अस्थायी, रेवल्यू रेफरेंस नहीं लौटना चाहिए), लेकिन, अगर कोई ऐसा करने वाला था, तो आप बनाना चाहते हैं। यकीन है कि आने वाले प्रतिद्वंद्विता-संदर्भ thisसूचक के रूप में एक ही वस्तु को संदर्भित नहीं कर रहे थे ।


ध्यान दें कि "a = std :: move (a)" यह स्थिति का एक तुच्छ तरीका है। आपका उत्तर यद्यपि मान्य है।
वॉन काटो

1
पूरी तरह से सहमत हूं कि यह सबसे आसान तरीका है, हालांकि मुझे लगता है कि ज्यादातर लोग जानबूझकर ऐसा नहीं करेंगे :-) ... हालांकि ध्यान रखें कि यदि रवैल्यू-संदर्भ है const, तो आप केवल इसे पढ़ सकते हैं, इसलिए केवल इसकी आवश्यकता है यदि आप में operator=(const T&&)से एक ही री-इनिशियलाइज़ेशन करने का निर्णय लिया है, तो आप एक चेक - thisइन करेंगे , जो कि आप एक विशिष्ट operator=(const T&)विधि में करेंगे बजाय एक स्वैपिंग-स्टाइल ऑपरेशन (यानी, गहरी कॉपी करने के बजाय पॉइंटर्स आदि चोरी करना)।
जेसन

1

मेरा जवाब अभी भी है कि चाल असाइनमेंट को आत्मसात के खिलाफ बचाने की जरूरत नहीं है, लेकिन इसकी एक अलग व्याख्या है। Std पर विचार करें: unique_ptr। अगर मुझे एक को लागू करना था, तो मैं कुछ इस तरह से करूंगा:

unique_ptr& operator=(unique_ptr&& x) {
  delete ptr_;
  ptr_ = x.ptr_;
  x.ptr_ = nullptr;
  return *this;
}

यदि आप स्कॉट मेयर्स को यह समझाते हुए देखें कि वह कुछ ऐसा ही करता है। (यदि आप भटकते हैं तो स्वैप क्यों नहीं करना है - इसका एक अतिरिक्त लेखन है)। और यह सेल्फ असाइनमेंट के लिए सुरक्षित नहीं है।

कभी-कभी यह दुर्भाग्यपूर्ण होता है। वेक्टर के सभी सम संख्याओं से बाहर जाने पर विचार करें:

src.erase(
  std::partition_copy(src.begin(), src.end(),
                      src.begin(),
                      std::back_inserter(even),
                      [](int num) { return num % 2; }
                      ).first,
  src.end());

यह पूर्णांकों के लिए ठीक है, लेकिन मुझे विश्वास नहीं है कि आप इस कार्य को चाल शब्दार्थ के साथ कुछ बना सकते हैं।

निष्कर्ष निकालने के लिए: ऑब्जेक्ट को असाइनमेंट ले जाना स्वयं ठीक नहीं है और आपको इसके लिए देखना होगा।

छोटा अपडेट।

  1. मैं हावर्ड से असहमत हूं, जो एक बुरा विचार है, लेकिन फिर भी - मुझे लगता है कि "स्थानांतरित" वस्तुओं के स्व चालन कार्य को काम करना चाहिए, क्योंकि swap(x, x)काम करना चाहिए। एल्गोरिदम इन चीजों से प्यार करते हैं! यह हमेशा अच्छा होता है जब एक कोने का मामला काम करता है। (और मैं अभी तक एक ऐसे मामले को देख रहा हूं जहां यह मुफ्त में नहीं है। इसका मतलब यह नहीं है कि यह मौजूद नहीं है)।
  2. यह इस तरह है कि यूनीक_पार्ट्स असाइन करना libc ++ में लागू किया गया है: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} यह सेल्फ मूव असाइनमेंट के लिए सुरक्षित है।
  3. कोर दिशानिर्देशों को लगता है कि यह सेल्फ मूव असाइन करने के लिए ठीक होना चाहिए।

0

ऐसी स्थिति है कि (यह == rhs) मैं सोच सकता हूं। इस कथन के लिए: मायक्लास ओब्ज; std :: Move (obj) = std :: Move (obj)


मायक्लास ओबज; std :: move (obj) = std :: Move (obj);
Little_monster
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.