चाल शब्दार्थ क्या है?


1700

मैंने अभी C ++ 0x के बारे में स्कॉट मेयर्स के साथ सॉफ्टवेयर इंजीनियरिंग रेडियो पॉडकास्ट साक्षात्कार को सुनना समाप्त कर दिया है । अधिकांश नई विशेषताओं ने मुझे समझ में आया, और मैं वास्तव में C ++ 0x के बारे में उत्साहित हूं, एक के अपवाद के साथ। मुझे अभी भी चाल शब्दार्थ नहीं मिला ... यह वास्तव में क्या है?


20
मैंने [एली बेन्द्स्की के ब्लॉग लेख] ( eli.thegreenplace.net/2011/12/15/… ) के बारे में C और C ++ में काफी जानकारीपूर्ण जानकारी दी। उन्होंने C ++ 11 में संदर्भों का भी उल्लेख किया है और उन्हें छोटे उदाहरणों के साथ पेश किया है।
Nils


19
हर साल या तो मुझे आश्चर्य होता है कि C ++ में "नया" चाल शब्दार्थ क्या है, मैं इसे गूगल करता हूं और इस पृष्ठ पर पहुंचता हूं। मैं प्रतिक्रियाओं को पढ़ता हूं, मेरा मस्तिष्क बन्द हो जाता है। मैं सी पर वापस जाता हूं, और सब कुछ भूल जाता हूं! मेरा गतिरोध हो रहा है।
आकाश

7
@sky std :: वेक्टर <> पर विचार करें ... कहीं न कहीं ढेर पर एक सरणी के लिए एक संकेतक है। यदि आप इस ऑब्जेक्ट को कॉपी करते हैं तो एक नया बफर आवंटित किया जाना है और बफर से डेटा को नए बफर में कॉपी करने की आवश्यकता है। क्या ऐसी कोई परिस्थिति है जहाँ केवल सूचक को चुराना ठीक होगा? उत्तर हां है, जब संकलक जानता है कि वस्तु अस्थायी है। मूवमेंट शब्दार्थ आपको यह परिभाषित करने की अनुमति देता है कि आपकी कक्षाओं की हिम्मत कैसे बाहर निकाली जा सकती है और एक अलग ऑब्जेक्ट में गिराई जा सकती है जब कंपाइलर को पता है कि जिस ऑब्जेक्ट से आप आगे बढ़ रहे हैं वह दूर जाने वाला है।
dicroce

एकमात्र संदर्भ जो मैं समझ सकता हूं: learncpp.com/cpp-tutorial/… , अर्थात मूवमेंट शब्दार्थ का मूल तर्क स्मार्ट पॉइंटर्स से है।
jw_

जवाबों:


2478

मुझे उदाहरण कोड के साथ चाल शब्दार्थ को समझना आसान है। आइए एक बहुत ही सरल स्ट्रिंग कक्षा से शुरू करें जो केवल एक संकेत को स्मृति के ढेर-आवंटित ब्लॉक में रखती है:

#include <cstring>
#include <algorithm>

class string
{
    char* data;

public:

    string(const char* p)
    {
        size_t size = std::strlen(p) + 1;
        data = new char[size];
        std::memcpy(data, p, size);
    }

चूंकि हमने स्वयं मेमोरी का प्रबंधन करना चुना था, इसलिए हमें तीन के नियम का पालन ​​करना होगा । मैं असाइनमेंट ऑपरेटर लिखना स्थगित करने जा रहा हूं और केवल विध्वंसक और अब के लिए कॉपी कंस्ट्रक्टर को लागू करूंगा:

    ~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = std::strlen(that.data) + 1;
        data = new char[size];
        std::memcpy(data, that.data, size);
    }

कॉपी कंस्ट्रक्टर परिभाषित करता है कि स्ट्रिंग ऑब्जेक्ट्स को कॉपी करने का क्या मतलब है। पैरामीटर const string& thatसभी प्रकार के स्ट्रिंग को बांधता है जो आपको निम्नलिखित उदाहरणों में प्रतियां बनाने की अनुमति देता है:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

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

लाइनों 2 और 3 में तर्क नहीं हैं, लेकिन लकीरें हैं, क्योंकि अंतर्निहित स्ट्रिंग वस्तुओं का कोई नाम नहीं है, इसलिए क्लाइंट के पास बाद में समय पर फिर से निरीक्षण करने का कोई तरीका नहीं है। rvalues ​​अस्थायी वस्तुओं को दर्शाते हैं जो अगले अर्धविराम पर नष्ट हो जाते हैं (अधिक सटीक होने के लिए: पूर्ण-अभिव्यक्ति के अंत में जो कि शाब्दिक रूप से अंतराल होता है)। यह महत्वपूर्ण है क्योंकि bऔर के आरंभीकरण के दौरान c, हम स्रोत स्ट्रिंग के साथ जो चाहें कर सकते थे, और ग्राहक अंतर नहीं बता सकते थे !

C ++ 0x "रैवल्यू रेफरेंस" नामक एक नए तंत्र का परिचय देता है, जो अन्य बातों के अलावा, हमें फ़ंक्शन ओवरलोडिंग के माध्यम से प्रतिद्वंद्विता के तर्कों का पता लगाने की अनुमति देता है। हमें बस इतना करना है कि एक रेवल्यू रेफरेंस पैरामीटर के साथ एक कंस्ट्रक्टर लिखना है। उस कंस्ट्रक्टर के अंदर हम स्रोत के साथ कुछ भी कर सकते हैं , जब तक कि हम इसे कुछ वैध स्थिति में छोड़ देते हैं :

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

हमने यहाँ क्या किया है? ढेर डेटा को गहराई से कॉपी करने के बजाय, हमने सिर्फ पॉइंटर को कॉपी किया है और फिर मूल पॉइंटर को नल ('डिलीट [] को रोकने के लिए सोर्स ऑब्जेक्ट के डिस्ट्रक्टर को हमारे' बस चुराए हुए डेटा 'को रिलीज करने से रोक दिया है)। वास्तव में, हमने डेटा को "चुराया" है जो मूल रूप से स्रोत स्ट्रिंग से संबंधित है। फिर, मुख्य अंतर्दृष्टि यह है कि किसी भी परिस्थिति में ग्राहक यह पता नहीं लगा सकता है कि स्रोत को संशोधित किया गया था। चूंकि हम वास्तव में यहां एक प्रति नहीं करते हैं, इसलिए हम इस निर्माता को "मूव कंस्ट्रक्टर" कहते हैं। इसका काम संसाधनों को कॉपी करने के बजाय एक वस्तु से दूसरी वस्तु में ले जाना है।

बधाई हो, अब आप चाल शब्दार्थ की मूल बातें समझ गए हैं! आइए असाइनमेंट ऑपरेटर को लागू करके जारी रखें। यदि आप कॉपी और स्वैप मुहावरे से अपरिचित हैं , तो इसे सीखें और वापस आएं, क्योंकि यह अपवाद सुरक्षा से संबंधित एक भयानक C ++ मुहावरा है।

    string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

हुह, यह है? "कहाँ एक संदर्भ संदर्भ है?" आप पूछ सकते हैं। "हमें यहाँ इसकी आवश्यकता नहीं है!" मेरा जवाब है :)

ध्यान दें कि हम पैरामीटर that को मान से पास करते हैं , इसलिए thatइसे किसी भी अन्य स्ट्रिंग ऑब्जेक्ट की तरह इनिशियलाइज़ करना होगा। वास्तव में कैसे thatशुरू किया जा रहा है? C ++ 98 के पुराने दिनों में , जवाब "कॉपी कंस्ट्रक्टर द्वारा" होता। C ++ 0x में, कंपाइलर कॉपी कंस्ट्रक्टर और मूव कंस्ट्रक्टर के बीच चुनता है कि असाइनमेंट ऑपरेटर का तर्क एक लवल्यू है या कोई रेवले।

इसलिए यदि आप कहते हैं a = b, तो कॉपी कंस्ट्रक्टर इनिशियलाइज़ करेगा that(क्योंकि एक्सप्रेशन bएक लवल्यू है), और असाइनमेंट ऑपरेटर एक नए सिरे से बनाई गई डीप कॉपी के साथ कंटेंट को स्वैप करता है। यह कॉपी की बहुत परिभाषा है और मुहावरा मुक्का मारना - एक प्रति बनाना, कॉपी के साथ सामग्री को स्वैप करना और फिर स्कोप को छोड़ कर कॉपी से छुटकारा पाना। यहां कुछ भी नया नहीं है।

लेकिन अगर आप कहते हैं a = x + y, तो मूव कंस्ट्रक्टर इनिशियलाइज़ हो जाएगा that(क्योंकि एक्सप्रेशन x + yएक प्रतिद्वंद्विता है), इसलिए इसमें कोई गहरी कॉपी शामिल नहीं है, केवल एक कुशल कदम है। thatअभी भी तर्क से एक स्वतंत्र वस्तु है, लेकिन इसका निर्माण तुच्छ था, क्योंकि ढेर डेटा को कॉपी नहीं करना था, बस चले गए। इसे कॉपी करना आवश्यक नहीं था क्योंकि x + yएक प्रतिद्वंद्विता है, और फिर से, यह स्ट्रिंग द्वारा निरूपित स्ट्रिंग ऑब्जेक्ट्स से स्थानांतरित करने के लिए ठीक है।

संक्षेप में, कॉपी कंस्ट्रक्टर एक गहरी प्रतिलिपि बनाता है, क्योंकि स्रोत को अछूता रहना चाहिए। दूसरी ओर, चाल निर्माणकर्ता, केवल पॉइंटर को कॉपी कर सकता है और फिर पॉइंटर को अशक्त करने के लिए स्रोत में सेट कर सकता है। इस तरीके से स्रोत ऑब्जेक्ट को "शून्य" करना ठीक है, क्योंकि क्लाइंट के पास फिर से ऑब्जेक्ट का निरीक्षण करने का कोई तरीका नहीं है।

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


40
@ लेकिन अगर मेरे ctor को कोई rvalue मिल रहा है, जिसे बाद में कभी इस्तेमाल नहीं किया जा सकता है, तो मुझे इसे लगातार / सुरक्षित स्थिति में छोड़ने की आवश्यकता क्यों है? इसके बजाय that.data = 0 को सेट करने के बजाय, बस इसे क्यों नहीं छोड़ना चाहिए?
ईनपोकलुम

70
@einpoklum क्योंकि इसके बिना that.data = 0, अक्षर बहुत जल्दी नष्ट हो जाएंगे (जब अस्थायी मृत्यु हो जाती है), और दो बार भी। आप डेटा चोरी करना चाहते हैं, इसे साझा नहीं करना चाहते हैं!
फ्रेडओवरफ्लो

19
@einpoklum नियमित रूप से शेड्यूल किए गए डिस्ट्रक्टर अभी भी चलते हैं, इसलिए आपको यह सुनिश्चित करना होगा कि स्रोत ऑब्जेक्ट का पोस्ट-मूव स्टेट क्रैश न हो। बेहतर है, आपको यह सुनिश्चित करना चाहिए कि स्रोत ऑब्जेक्ट एक असाइनमेंट या अन्य लेखन का रिसीवर भी हो सकता है।
CTMacUser

12
@pranitkothari हां, सभी वस्तुओं को नष्ट कर दिया जाना चाहिए, यहां तक ​​कि वस्तुओं से भी स्थानांतरित किया जाना चाहिए। और जब से हम नहीं चाहते हैं कि जब यह होता है तो चार सरणी को हटा दिया जाए, हमें सूचक को अशक्त करना होगा।
fredoverflow

7
Nullptr delete[]पर @ Virus721 को C ++ मानक द्वारा एक नो-ऑप के रूप में परिभाषित किया गया है।
fredoverflow

1057

मेरा पहला उत्तर शब्दार्थ को स्थानांतरित करने के लिए एक अत्यंत सरलीकृत परिचय था, और इसे सरल रखने के उद्देश्य से कई विवरण छोड़ दिए गए थे। हालांकि, शब्दार्थ को स्थानांतरित करने के लिए बहुत कुछ है, और मुझे लगा कि अंतराल को भरने के लिए एक दूसरे उत्तर के लिए समय था। पहला उत्तर पहले से ही काफी पुराना है, और इसे पूरी तरह से अलग पाठ के साथ बदलने का अधिकार नहीं था। मुझे लगता है कि यह अभी भी पहले परिचय के रूप में अच्छी तरह से कार्य करता है। लेकिन अगर आप गहरी खुदाई करना चाहते हैं, तो पढ़ें :)

स्टीफन टी। लावेज ने बहुमूल्य प्रतिक्रिया प्रदान करने के लिए समय लिया। बहुत बहुत धन्यवाद, स्टेफ़न!

परिचय

मूवमेंट शब्दार्थ एक वस्तु को कुछ शर्तों के तहत, किसी अन्य वस्तु के बाहरी संसाधनों का स्वामित्व लेने की अनुमति देता है। यह दो तरह से महत्वपूर्ण है:

  1. महंगी प्रतियों को सस्ते चालों में बदलना। एक उदाहरण के लिए मेरा पहला उत्तर देखें। ध्यान दें कि यदि कोई वस्तु कम से कम एक बाहरी संसाधन (या तो प्रत्यक्ष या अप्रत्यक्ष रूप से अपने सदस्य वस्तुओं के माध्यम से) का प्रबंधन नहीं करती है, तो स्थानांतरित शब्दार्थ कॉपी प्रति शब्द विज्ञान पर कोई लाभ नहीं देगा। उस स्थिति में, किसी वस्तु की नकल करना और किसी वस्तु को ले जाने का मतलब ठीक उसी चीज से है:

    class cannot_benefit_from_move_semantics
    {
        int a;        // moving an int means copying an int
        float b;      // moving a float means copying a float
        double c;     // moving a double means copying a double
        char d[64];   // moving a char array means copying a char array
    
        // ...
    };
    
  2. सुरक्षित "चाल-केवल" प्रकारों को लागू करना; यह है, प्रकार जिसके लिए नकल मतलब नहीं है, लेकिन चलती है। उदाहरणों में अद्वितीय स्वामित्व वाले शब्दार्थ के साथ ताले, फ़ाइल हैंडल और स्मार्ट पॉइंटर्स शामिल हैं। नोट: यह उत्तर चर्चा करता है std::auto_ptr, एक उपेक्षित C ++ 98 मानक पुस्तकालय टेम्पलेट, जिसे std::unique_ptrC ++ 11 द्वारा प्रतिस्थापित किया गया था । इंटरमीडिएट सी ++ प्रोग्रामर शायद कम से कम कुछ हद तक परिचित हैं std::auto_ptr, और "चालित अर्थ विज्ञान" के कारण यह प्रदर्शित करता है, यह सी ++ 11 में चाल शब्दार्थों पर चर्चा करने के लिए एक अच्छी शुरुआत की तरह लगता है। YMMV।

एक चाल क्या है?

C ++ 98 मानक पुस्तकालय एक स्मार्ट पॉइंटर प्रदान करता है जिसे अद्वितीय स्वामित्व शब्दार्थ कहा जाता है std::auto_ptr<T>। यदि आप इससे अपरिचित हैं auto_ptr, तो इसका उद्देश्य यह सुनिश्चित करना है कि एक गतिशील रूप से आवंटित वस्तु हमेशा जारी होती है, यहां तक ​​कि अपवादों के सामने भी:

{
    std::auto_ptr<Shape> a(new Triangle);
    // ...
    // arbitrary code, could throw exceptions
    // ...
}   // <--- when a goes out of scope, the triangle is deleted automatically

auto_ptrइसके "नकल" व्यवहार के बारे में असामान्य बात है:

auto_ptr<Shape> a(new Triangle);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        |
        |
  +-----|---+
  |   +-|-+ |
a | p | | | |
  |   +---+ |
  +---------+

auto_ptr<Shape> b(a);

      +---------------+
      | triangle data |
      +---------------+
        ^
        |
        +----------------------+
                               |
  +---------+            +-----|---+
  |   +---+ |            |   +-|-+ |
a | p |   | |          b | p | | | |
  |   +---+ |            |   +---+ |
  +---------+            +---------+

ध्यान दें कि कैसे के bसाथ आरंभ त्रिकोण को कॉपी नहींa करता है , बल्कि इसके बजाय से त्रिकोण के स्वामित्व को स्थानांतरित करता है । हम यह भी कह " है में चले गए या" त्रिकोण है " ले जाया गया से करने के लिए "। यह भ्रामक लग सकता है क्योंकि त्रिकोण हमेशा स्मृति में एक ही स्थान पर रहता है।aba ba b

किसी ऑब्जेक्ट को स्थानांतरित करने का अर्थ है कुछ संसाधन के स्वामित्व को किसी अन्य ऑब्जेक्ट को प्रबंधित करना।

कॉपी कंस्ट्रक्टर auto_ptrशायद कुछ इस तरह दिखता है (कुछ सरलीकृत):

auto_ptr(auto_ptr& source)   // note the missing const
{
    p = source.p;
    source.p = 0;   // now the source no longer owns the object
}

खतरनाक और हानिरहित चाल

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

auto_ptr<Shape> a(new Triangle);   // create triangle
auto_ptr<Shape> b(a);              // move a into b
double area = a->area();           // undefined behavior

लेकिन हमेशा खतरनाक auto_ptrनहीं होता है। कारखाने के कार्यों के लिए एक पूरी तरह से ठीक उपयोग के मामले हैं auto_ptr:

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

ध्यान दें कि दोनों उदाहरण एक ही वाक्यविन्यास पैटर्न का अनुसरण कैसे करते हैं:

auto_ptr<Shape> variable(expression);
double area = expression->area();

और फिर भी, उनमें से एक अपरिभाषित व्यवहार करता है, जबकि दूसरा नहीं करता है। तो भावों aऔर के बीच अंतर क्या है make_triangle()? क्या वे दोनों एक ही प्रकार के नहीं हैं? वास्तव में वे हैं, लेकिन उनकी अलग-अलग मूल्य श्रेणियां हैं

मूल्य श्रेणियों

जाहिर है, अभिव्यक्ति के बीच कुछ गहरा अंतर होना चाहिए aजो एक auto_ptrचर को दर्शाता है, और अभिव्यक्ति make_triangle()जो एक फ़ंक्शन के कॉल को निरूपित करता है जो एक auto_ptrमूल्य से लौटता है , इस प्रकार auto_ptrहर बार एक ताजा अस्थायी वस्तु का निर्माण होता है जिसे कहा जाता है। aएक का एक उदाहरण है lvalue जबकि, make_triangle()एक का एक उदाहरण है rvalue

इस तरह के रूप aमें खतरनाक है, क्योंकि हम बाद में एक सदस्य समारोह को फोन करने की कोशिश कर सकते हैं a, अपरिभाषित व्यवहार को छोड़कर । दूसरी ओर, प्रतिद्वंद्वियों से आगे बढ़ना जैसे कि make_triangle()पूरी तरह से सुरक्षित है, क्योंकि कॉपी कंस्ट्रक्टर ने अपना काम करने के बाद, हम अस्थायी रूप से फिर से उपयोग नहीं कर सकते हैं। कोई भी अभिव्यक्ति नहीं है जो कहा जाता है कि अस्थायी है; अगर हम बस make_triangle()फिर से लिखते हैं , तो हमें एक अलग अस्थायी मिलता है । वास्तव में, स्थानांतरित-से-अस्थायी पहले से ही अगली पंक्ति पर चला गया है:

auto_ptr<Shape> c(make_triangle());
                                  ^ the moved-from temporary dies right here

ध्यान दें कि अक्षर lऔर rबाएं हाथ की ओर एक असाइनमेंट के दाईं ओर एक ऐतिहासिक मूल है। C ++ में यह अब सही नहीं है, क्योंकि ऐसे कार्य हैं जो किसी असाइनमेंट के बाईं ओर नहीं दिखाई दे सकते हैं (जैसे कि असाइनमेंट ऑपरेटर के बिना सरणियाँ या उपयोगकर्ता-परिभाषित प्रकार), और ऐसे नियम हैं जो (सभी प्रकार के वर्ग प्रकार) एक असाइनमेंट ऑपरेटर के साथ)।

वर्ग प्रकार का एक प्रकारांतर एक अभिव्यक्ति है जिसका मूल्यांकन एक अस्थायी वस्तु बनाता है। सामान्य परिस्थितियों में, समान दायरे के अंदर कोई अन्य अभिव्यक्ति समान अस्थायी वस्तु को दर्शाता नहीं है।

रेवले संदर्भ

अब हम समझते हैं कि lvalues ​​से आगे बढ़ना संभावित खतरनाक है, लेकिन rvalues ​​से चलना हानिरहित है। तो सी ++ rvalue तर्कों से lvalue तर्क भेद करने के लिए भाषा समर्थन किया था, हम या तो पूरी तरह से lvalues से आगे बढ़ न करे सकता है, या कम से कम मेकअप lvalues से आगे बढ़ पर स्पष्ट दुर्घटना से इतना है कि हम अब इस कदम कॉल स्थल पर,।

C ++ 11 की इस समस्या का उत्तर संदर्भ संदर्भ है । एक रैवल्यू संदर्भ एक नए प्रकार का संदर्भ है जो केवल प्रतिद्वंद्वियों को बांधता है, और वाक्यविन्यास है X&&। अच्छा पुराना संदर्भ X&अब एक लवल्यू रेफरेंस के रूप में जाना जाता है । (ध्यान दें कि X&&है नहीं एक संदर्भ के लिए एक संदर्भ, वहाँ सी में ऐसी कोई बात नहीं है ++।)

यदि हम constमिश्रण में फेंकते हैं , तो हमारे पास पहले से ही चार अलग-अलग प्रकार के संदर्भ हैं। वे किस प्रकार के भावों से बंध Xसकते हैं?

            lvalue   const lvalue   rvalue   const rvalue
---------------------------------------------------------              
X&          yes
const X&    yes      yes            yes      yes
X&&                                 yes
const X&&                           yes      yes

व्यवहार में, आप भूल सकते हैं const X&&। प्रतिद्वंद्वियों से पढ़ने के लिए प्रतिबंधित होना बहुत उपयोगी नहीं है।

एक रैवल्यू संदर्भ X&&एक नए प्रकार का संदर्भ है जो केवल प्रतिद्वंद्वियों को बांधता है।

सम्यक रूपांतरण

रेवल्यू संदर्भ कई संस्करणों के माध्यम से चला गया। संस्करण 2.1 के बाद से, एक प्रतिद्वंद्विता संदर्भ X&&भी एक अलग प्रकार के सभी मूल्य श्रेणियों को बांधता है Y, बशर्ते इसमें से एक अंतर्निहित रूपांतरण Yहो X। उस स्थिति में, एक अस्थायी प्रकार Xबनाया जाता है, और प्रतिद्वंद्विता संदर्भ उस अस्थायी से जुड़ा होता है:

void some_function(std::string&& r);

some_function("hello world");

उपरोक्त उदाहरण में, "hello world"प्रकार का एक अंतराल है const char[12]। के बाद से वहाँ एक अंतर्निहित रूपांतरण से है const char[12]के माध्यम से const char*करने के लिए std::string, प्रकार की एक अस्थायी std::stringबनाई गई है, और rहै कि अस्थायी के लिए बाध्य है। यह उन मामलों में से एक है जहां प्रतिद्वंद्वियों (अभिव्यक्ति) और अस्थायी (वस्तुओं) के बीच का अंतर थोड़ा धुंधला है।

कंस्ट्रक्टरों को स्थानांतरित करें

एक X&&पैरामीटर के साथ फ़ंक्शन का एक उपयोगी उदाहरण मूव कंस्ट्रक्टर है X::X(X&& source) । इसका उद्देश्य स्रोत से प्रबंधित संसाधन के स्वामित्व को वर्तमान वस्तु में स्थानांतरित करना है।

C ++ 11 में, std::auto_ptr<T>को प्रतिस्थापित किया गया है , std::unique_ptr<T>जो कि रवैल्यू रेफरेंस का लाभ उठाता है। मैं एक सरलीकृत संस्करण का विकास और चर्चा करूंगा unique_ptr। सबसे पहले, हम एक कच्चे पॉइंटर को एनकैप्सुलेट करते हैं ->और ऑपरेटरों को अधिभारित करते हैं और *इसलिए हमारी कक्षा एक पॉइंटर की तरह महसूस करती है:

template<typename T>
class unique_ptr
{
    T* ptr;

public:

    T* operator->() const
    {
        return ptr;
    }

    T& operator*() const
    {
        return *ptr;
    }

कंस्ट्रक्टर ऑब्जेक्ट का स्वामित्व लेता है, और विध्वंसक इसे हटा देता है:

    explicit unique_ptr(T* p = nullptr)
    {
        ptr = p;
    }

    ~unique_ptr()
    {
        delete ptr;
    }

अब आता है दिलचस्प हिस्सा, मूव कंस्ट्रक्टर:

    unique_ptr(unique_ptr&& source)   // note the rvalue reference
    {
        ptr = source.ptr;
        source.ptr = nullptr;
    }

यह मूव कंस्ट्रक्टर वही करता है जो auto_ptrकॉपी कंस्ट्रक्टर ने किया था, लेकिन इसे केवल प्रतिद्वंद्वियों को ही दिया जा सकता है:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());   // okay

दूसरी पंक्ति संकलित करने में विफल रहती है, क्योंकि aएक अंतराल है, लेकिन पैरामीटर unique_ptr&& sourceकेवल प्रतिद्वंद्वियों के लिए बाध्य हो सकता है। यह वही है जो हम चाहते थे; खतरनाक चाल कभी भी निहित नहीं होनी चाहिए। तीसरी पंक्ति बस ठीक संकलन करती है, क्योंकि make_triangle()एक प्रतिद्वंद्विता है। चाल निर्माणकर्ता अस्थायी से स्वामित्व स्थानांतरित करेगा c। फिर, यह वही है जो हम चाहते थे।

चाल निर्माणकर्ता एक प्रबंधित संसाधन के स्वामित्व को वर्तमान वस्तु में स्थानांतरित करता है।

असाइनमेंट ऑपरेटरों को स्थानांतरित करें

अंतिम लापता टुकड़ा चाल असाइनमेंट ऑपरेटर है। इसका काम पुराने संसाधन को जारी करना और अपने तर्क से नए संसाधन प्राप्त करना है:

    unique_ptr& operator=(unique_ptr&& source)   // note the rvalue reference
    {
        if (this != &source)    // beware of self-assignment
        {
            delete ptr;         // release the old resource

            ptr = source.ptr;   // acquire the new resource
            source.ptr = nullptr;
        }
        return *this;
    }
};

ध्यान दें कि चाल असाइनमेंट ऑपरेटर का यह कार्यान्वयन विध्वंसक और चाल निर्माणकर्ता दोनों के तर्क की नकल करता है। क्या आप कॉपी-एंड-स्वैप मुहावरे से परिचित हैं? इसे मूवमेंट्स को मूव-एंड-स्वैप मुहावरे के रूप में लागू करने के लिए भी लागू किया जा सकता है:

    unique_ptr& operator=(unique_ptr source)   // note the missing reference
    {
        std::swap(ptr, source.ptr);
        return *this;
    }
};

अब sourceयह एक प्रकार का चर है unique_ptr, इसे मूव कंस्ट्रक्टर द्वारा इनिशियलाइज़ किया जाएगा; यही है, तर्क को पैरामीटर में ले जाया जाएगा। तर्क को अभी भी एक प्रतिद्वंद्विता होने की आवश्यकता है, क्योंकि इस कदम के निर्माणकर्ता के पास एक संदर्भ संदर्भ पैरामीटर है। नियंत्रण प्रवाह के समापन ब्रेस तक पहुँच जाता है operator=, sourceक्षेत्र से बाहर चला जाता है, पुराने संसाधन स्वचालित रूप से रिलीज़ किया था।

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

लवलीन से चल रहा है

कभी-कभी, हम lvalues ​​से आगे बढ़ना चाहते हैं। यही है, कभी-कभी हम चाहते हैं कि कंपाइलर एक लवल्यू का इलाज करे जैसे कि वह एक रव्वा था, इसलिए यह मूव कंस्ट्रक्टर को आमंत्रित कर सकता है, भले ही यह संभावित असुरक्षित हो सकता है। इस उद्देश्य के लिए, C ++ 11 std::moveहेडर के अंदर एक मानक पुस्तकालय फ़ंक्शन टेम्पलेट प्रदान करता है <utility>। यह नाम थोड़ा दुर्भाग्यपूर्ण है, क्योंकि std::moveबस एक प्रतिद्वंद्विता के लिए एक झूठ बोलता है; यह अपने आप कुछ नहीं ले जाता है। यह केवल गति करने में सक्षम बनाता है । शायद यह नाम दिया जाना चाहिए किया गया है std::cast_to_rvalueया std::enable_moveहै, लेकिन हम अब तक नाम के साथ फंस रहे हैं।

यहाँ बताया गया है कि आप स्पष्ट रूप से एक अंतराल से चलते हैं:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

ध्यान दें कि तीसरी पंक्ति के बाद, aअब एक त्रिकोण का मालिक नहीं है। यह ठीक है, क्योंकि स्पष्ट रूप से लिखने से std::move(a), हमने अपने इरादे स्पष्ट कर दिए: "प्रिय निर्माता, aआरंभ करने के लिए जो कुछ भी आप चाहते हैं c, मैं करता हूं; मुझे aअब कोई परवाह नहीं है। अपने तरीके से स्वतंत्र महसूस करें a।"

std::move(some_lvalue) इस तरह एक बाद के कदम को सक्षम करने के लिए, एक प्रतिद्वंद्विता के लिए एक अंतराल देता है।

XValues

ध्यान दें कि भले ही std::move(a)एक प्रतिद्वंद्विता हो, लेकिन इसका मूल्यांकन एक अस्थायी वस्तु नहीं बनाता है। इस कोन्ड्रम ने समिति को तीसरी मूल्य श्रेणी लाने के लिए मजबूर किया। कुछ ऐसा है जो एक पारंपरिक संदर्भ में नहीं हो सकता है, भले ही यह पारंपरिक अर्थों में एक प्रतिद्वंद्विता क्यों न हो , इसे एक xvalue (eXpiring value) कहा जाता है । पारंपरिक प्रतिद्वंद्वियों का नाम बदलकर प्रचलन (शुद्ध नियम) कर दिया गया ।

प्रचलन और xvalues ​​दोनों ही प्रचलन हैं। Xvalues ​​और lvalues ​​दोनों glvalues (Generalized lvalues) हैं। आरेख के साथ रिश्तों को समझना आसान है:

        expressions
          /     \
         /       \
        /         \
    glvalues   rvalues
      /  \       /  \
     /    \     /    \
    /      \   /      \
lvalues   xvalues   prvalues

ध्यान दें कि केवल xvalues ​​वास्तव में नए हैं; बाकी सिर्फ नाम बदलने और समूहीकरण के कारण है।

C ++ 11 में C ++ 98 प्रचलन को प्रचलन के रूप में जाना जाता है। मानसिक रूप से "व्याप्त" की सभी घटनाओं को पूर्ववर्ती पैराग्राफों में "प्रचलन" से बदल दें।

कार्यों से बाहर जा रहा है

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

unique_ptr<Shape> make_triangle()
{
    return unique_ptr<Shape>(new Triangle);
}          \-----------------------------/
                  |
                  | temporary is moved into c
                  |
                  v
unique_ptr<Shape> c(make_triangle());

शायद आश्चर्य की बात, स्वत: वस्तुओं (स्थानीय चर कि के रूप में घोषित नहीं कर रहे हैं static) भी किया जा सकता है परोक्ष कार्यों से बाहर चले गए:

unique_ptr<Shape> make_square()
{
    unique_ptr<Shape> result(new Square);
    return result;   // note the missing std::move
}

कैसे आता है कदम निर्माता resultतर्क के रूप में अंतराल को स्वीकार करता है? का दायरा resultसमाप्त होने वाला है, और स्टैक अनइंडिंग के दौरान यह नष्ट हो जाएगा। कोई भी संभवतः बाद में शिकायत नहीं कर सकता था जो resultकिसी भी तरह बदल गया था; जब नियंत्रण प्रवाह वापस कॉलर पर है, resultअब मौजूद नहीं है! इस कारण से, C ++ 11 में एक विशेष नियम है, जो बिना लिखने के कार्यों से स्वचालित वस्तुओं को वापस करने की अनुमति देता है std::move। वास्तव में, आपको स्वचालित ऑब्जेक्ट्स को फ़ंक्शंस से बाहर जाने के लिए उपयोग नहीं करना चाहिए std::move, क्योंकि यह "नाम वापसी मूल्य अनुकूलन" (NRVO) को रोकता है।

std::moveस्वचालित ऑब्जेक्ट्स को फ़ंक्शंस से बाहर ले जाने के लिए कभी भी उपयोग न करें ।

ध्यान दें कि दोनों कारखाने के कार्यों में, वापसी प्रकार एक मूल्य है, न कि एक संदर्भ संदर्भ। संदर्भ संदर्भ अभी भी संदर्भ हैं, और हमेशा की तरह, आपको कभी भी स्वचालित वस्तु का संदर्भ नहीं देना चाहिए; यदि आप संकलक को अपने कोड को स्वीकार करने में धोखा देते हैं, तो कॉलर झूलते हुए संदर्भ के साथ समाप्त होगा:

unique_ptr<Shape>&& flawed_attempt()   // DO NOT DO THIS!
{
    unique_ptr<Shape> very_bad_idea(new Square);
    return std::move(very_bad_idea);   // WRONG!
}

कभी भी स्वचालित वस्तुओं को संदर्भ के संदर्भ में न लौटाएँ। मूव एक्सक्लूसिव रूप से मूव कंस्ट्रक्टर द्वारा मूव किया जाता है, न कि std::moveरिविल्यू रेफरेंस से केवल एक रव्वा को बांधकर।

सदस्यों में चल रहा है

जल्दी या बाद में, आप इस तरह कोड लिखने जा रहे हैं:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}
};

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

एक नामांकित संदर्भ संदर्भ किसी अन्य चर की तरह एक अंतराल है।

समाधान मैन्युअल रूप से इस कदम को सक्षम करने के लिए है:

class Foo
{
    unique_ptr<Shape> member;

public:

    Foo(unique_ptr<Shape>&& parameter)
    : member(std::move(parameter))   // note the std::move
    {}
};

आप यह तर्क दे सकते हैं कि parameterइनका उपयोग शुरू होने के बाद अब नहीं किया जाता है memberstd::moveकेवल रिटर्न वैल्यू के साथ चुपचाप डालने का कोई विशेष नियम क्यों नहीं है ? शायद इसलिए, क्योंकि यह कंपाइलर कार्यान्वयनकर्ताओं पर बहुत अधिक बोझ होगा। उदाहरण के लिए, क्या होगा यदि निर्माणकर्ता निकाय किसी अन्य अनुवाद इकाई में था? इसके विपरीत, वापसी मान नियम को केवल यह निर्धारित करने के लिए प्रतीक तालिकाओं की जांच करनी है कि returnकीवर्ड स्वत: ऑब्जेक्ट को दर्शाता है या नहीं ।

आप parameterमूल्य से भी गुजर सकते हैं । मूव-ओनली टाइप्स के लिए unique_ptr, ऐसा लगता है कि अभी तक कोई स्थापित मुहावरा नहीं है। व्यक्तिगत रूप से, मैं मूल्य से पारित करना पसंद करता हूं, क्योंकि यह इंटरफ़ेस में कम अव्यवस्था का कारण बनता है।

विशेष सदस्य के कार्य

C ++ 98 स्पष्ट रूप से मांग पर तीन विशेष सदस्य कार्यों की घोषणा करता है, जब उन्हें कहीं जरूरत होती है: कॉपी कंस्ट्रक्टर, कॉपी असाइनमेंट ऑपरेटर और डिस्ट्रक्टर।

X::X(const X&);              // copy constructor
X& X::operator=(const X&);   // copy assignment operator
X::~X();                     // destructor

रेवल्यू संदर्भ कई संस्करणों के माध्यम से चला गया। संस्करण 3.0 के बाद से, C ++ 11 मांग पर दो अतिरिक्त विशेष सदस्य कार्य करता है: मूव कंस्ट्रक्टर और मूव असाइनमेंट ऑपरेटर। ध्यान दें कि न तो VC10 और न ही VC11 अभी तक 3.0 संस्करण के अनुरूप हैं, इसलिए आपको उन्हें स्वयं लागू करना होगा।

X::X(X&&);                   // move constructor
X& X::operator=(X&&);        // move assignment operator

इन दो नए विशेष सदस्य कार्यों को केवल तब ही घोषित किया जाता है जब विशेष सदस्य कार्यों में से कोई भी मैन्युअल रूप से घोषित नहीं किया जाता है। इसके अलावा, यदि आप अपने स्वयं के मूव कंस्ट्रक्टर या मूव असाइनमेंट ऑपरेटर की घोषणा करते हैं, तो न तो कॉपी कंस्ट्रक्टर और न ही कॉपी असाइनमेंट ऑपरेटर को स्पष्ट रूप से घोषित किया जाएगा।

व्यवहार में इन नियमों का क्या मतलब है?

यदि आप अप्रबंधित संसाधनों के बिना एक वर्ग लिखते हैं, तो पांच विशेष सदस्य कार्यों में से किसी को भी स्वयं घोषित करने की आवश्यकता नहीं है, और आपको सही कॉपी शब्दार्थ मिलेगा और मुफ्त में शब्दार्थ को स्थानांतरित करना होगा। अन्यथा, आपको विशेष सदस्य कार्यों को स्वयं लागू करना होगा। बेशक, यदि आपकी कक्षा चालित शब्दार्थ से लाभ नहीं उठाती है, तो विशेष कदम संचालन को लागू करने की कोई आवश्यकता नहीं है।

ध्यान दें कि कॉपी असाइनमेंट ऑपरेटर और मूव असाइनमेंट ऑपरेटर को सिंगल, यूनिफाइड असाइनमेंट ऑपरेटर में फ्यूज किया जा सकता है, मान द्वारा इसकी समीक्षा करें:

X& X::operator=(X source)    // unified assignment operator
{
    swap(source);            // see my first answer for an explanation
    return *this;
}

इस तरह, विशेष सदस्य की संख्या पांच से चार तक की बूंदों को लागू करने के लिए कार्य करती है। यहां अपवाद-सुरक्षा और दक्षता के बीच एक व्यापार है, लेकिन मैं इस मुद्दे पर विशेषज्ञ नहीं हूं।

अग्रेषण संदर्भ ( पहले सार्वभौमिक संदर्भ के रूप में जाना जाता है )

निम्नलिखित फ़ंक्शन टेम्पलेट पर विचार करें:

template<typename T>
void foo(T&&);

आप T&&केवल प्रतिद्वंद्वियों से बंधने की उम्मीद कर सकते हैं , क्योंकि पहली नज़र में, यह एक संदर्भ के संदर्भ की तरह दिखता है। हालांकि यह पता चला है, T&&यह भी अंतराल को बांधता है:

foo(make_triangle());   // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a);                 // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&

यदि तर्क प्रकार का एक खंड है X, Tतो होने का मतलब है X, इसलिए T&&इसका मतलब है X&&। यह वही है जो कोई भी उम्मीद करेगा। लेकिन अगर तर्क Xएक विशेष नियम के कारण, प्रकार का एक प्रकार है , तो होने के लिए कटौती की Tजाती है X&, इसलिए T&&इसका मतलब कुछ ऐसा होगा X& &&। लेकिन चूंकि C ++ में अभी भी संदर्भों के संदर्भ में कोई धारणा नहीं है, इसलिए टाइप X& &&को ढहा दिया गया है X&। यह पहली बार में भ्रामक और बेकार लग सकता है, लेकिन परफेक्ट फ़ॉरवर्डिंग के लिए संदर्भ ढहना आवश्यक है (जिसकी चर्चा यहां नहीं की जाएगी)।

T && एक संदर्भ संदर्भ नहीं है, बल्कि एक अग्रेषण संदर्भ है। यह भी अंतराल को बांधता है, जिस स्थिति में Tऔर T&&दोनों अंतराल संदर्भ हैं।

यदि आप किसी फ़ंक्शन टेम्प्लेट को प्रतिद्वंद्वियों के लिए बाध्य करना चाहते हैं, तो आप SFINAE को टाइप लक्षणों के साथ जोड़ सकते हैं :

#include <type_traits>

template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);

चाल का क्रियान्वयन

अब जब आप संदर्भ ढहने को समझते हैं, तो यहां बताया गया std::moveहै:

template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
    return static_cast<typename std::remove_reference<T>::type&&>(t);
}

जैसा कि आप देख सकते हैं, moveअग्रेषण संदर्भ के लिए किसी भी प्रकार के पैरामीटर को स्वीकार करता है T&&, और यह एक रवैल्यू संदर्भ देता है। std::remove_reference<T>::typeक्योंकि अन्यथा, प्रकार की lvalues के लिए मेटा-समारोह कॉल के लिए आवश्यक है X, वापसी प्रकार होगा X& &&, जो टुकड़ों में टूट जाएगा X&। चूँकि tहमेशा एक अंतराल है (याद रखें कि एक नामित रूवल संदर्भ एक अंतराल है), लेकिन हम tएक अंतराल संदर्भ को बांधना चाहते हैं , हमें स्पष्ट रूप tसे सही रिटर्न प्रकार में डालना होगा। एक फ़ंक्शन का कॉल जो एक रैवल्यू संदर्भ देता है, वह स्वयं एक xvalue है। अब आप जानते हैं कि xvalues ​​कहाँ से आते हैं;)

एक फ़ंक्शन का कॉल std::move, जो एक रैवल्यू संदर्भ देता है, जैसे कि , एक xvalue है।

ध्यान दें कि प्रतिद्वंद्विता संदर्भ द्वारा वापस लौटना इस उदाहरण में ठीक है, क्योंकि tएक स्वचालित वस्तु को निरूपित नहीं करता है, बल्कि कॉल करने वाले द्वारा पारित की गई वस्तु के बजाय।



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

8
मैं 'यूनिवर्सल रेफरेंस' के लिए आपके साथ था, लेकिन तब यह सब अमूर्त है। संदर्भ ढह रहा है? बिल्कुल सही अग्रेषण? क्या आप कह रहे हैं कि यदि टाइप किया जाता है, तो एक रेवल्यू संदर्भ एक सार्वभौमिक संदर्भ बन जाता है? काश, यह समझाने का कोई तरीका होता, ताकि मुझे पता चलता कि मुझे इसे समझने की ज़रूरत है या नहीं! :)
काइलोटन

8
कृपया अब एक पुस्तक लिखें ... इस उत्तर ने मुझे यह विश्वास करने का कारण दिया है कि क्या आप सी ++ के अन्य कोनों को इस तरह आकर्षक तरीके से कवर करते हैं, हजारों और लोग इसे समझेंगे।
आधा सफ़र 27'15

12
@halivingston अपनी तरह की प्रतिक्रिया के लिए बहुत बहुत धन्यवाद, मैं वास्तव में इसकी सराहना करता हूं। पुस्तक लिखने के साथ समस्या यह है: यह बहुत अधिक काम है जितना आप संभवतः कल्पना कर सकते हैं। यदि आप C ++ 11 और उससे अधिक में गहरी खुदाई करना चाहते हैं, तो मैं आपको स्कॉट मेयर्स द्वारा "प्रभावी आधुनिक C ++" खरीदने का सुझाव देता हूं।
fredoverflow

77

मूव शब्दार्थ रवले संदर्भों पर आधारित हैं ।
एक प्रतिद्वंद्विता एक अस्थायी वस्तु है, जो अभिव्यक्ति के अंत में नष्ट होने वाली है। वर्तमान C ++ में, संदर्भों को केवल constसंदर्भ में बाँधते हैं । C ++ 1x गैर- constरेवल्यू संदर्भों की अनुमति देगा , वर्तनी T&&, जो एक रिवैल्यू ऑब्जेक्ट्स के संदर्भ हैं।
चूंकि एक अभिव्यक्ति के अंत में एक प्रतिद्वंद्विता मरने वाली है, आप इसके डेटा को चुरा सकते हैं । इसे किसी अन्य ऑब्जेक्ट में कॉपी करने के बजाय , आप इसके डेटा को इसमें स्थानांतरित करते हैं।

class X {
public: 
  X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
    : data_()
  {
     // since 'x' is an rvalue object, we can steal its data
     this->swap(std::move(rhs));
     // this will leave rhs with the empty data
  }
  void swap(X&& rhs);
  // ... 
};

// ...

X f();

X x = f(); // f() returns result as rvalue, so this calls move-ctor

उपरोक्त कोड में, पुराने compilers के साथ का परिणाम f()है की नकल की में xका उपयोग कर Xके प्रति निर्माता। यदि आपका कंपाइलर चालित शब्दार्थ का समर्थन करता है और Xएक चाल-निर्माणकर्ता है, तो उसे इसके बजाय कहा जाता है। चूंकि इसका rhsतर्क एक प्रतिद्वंद्विता है , हम जानते हैं कि इसकी अब आवश्यकता नहीं है और हम इसका मूल्य चुरा सकते हैं।
तो मूल्य है ले जाया गया अनाम अस्थायी से लौटे से f()करने के लिए x(जब से डेटा x, एक खाली करने के लिए प्रारंभ X, अस्थायी है, जो काम के बाद नष्ट हो जाएगी में ले जाया जाता)।


1
ध्यान दें कि ऐसा this->swap(std::move(rhs));इसलिए होना चाहिए क्योंकि नामांकित संदर्भ रेवले होते हैं
वाममरक

इस थोड़े गलत है, प्रति @ Tacyt की टिप्पणी: rhsएक है lvalue के संदर्भ में X::X(X&& rhs)। आपको कॉल std::move(rhs)करने की आवश्यकता है , लेकिन एक उत्तर देने के लिए यह थोथा उत्तर देता है।
अशरह

पॉइंटर्स के बिना प्रकारों के लिए शब्दार्थ क्या है? मूवमेंट का काम करता है मूव कॉपी?
गुसेव स्लाव

@Gusev: मुझे नहीं पता कि तुम क्या पूछ रहे हो।
sbi

60

मान लें कि आपके पास एक फ़ंक्शन है जो एक पर्याप्त वस्तु लौटाता है:

Matrix multiply(const Matrix &a, const Matrix &b);

जब आप इस तरह कोड लिखते हैं:

Matrix r = multiply(a, b);

फिर एक साधारण C ++ कंपाइलर, परिणाम के लिए एक अस्थायी ऑब्जेक्ट बनाएगा multiply(), कॉपी कंस्ट्रक्टर को इनिशियलाइज़ करने के लिए कॉल करेगा r, और फिर अस्थायी रिटर्न वैल्यू को नष्ट कर देगा। C ++ 0x में शब्दार्थ को स्थानांतरित करें " rकंटेंट कंस्ट्रक्टर" को इसकी सामग्री को कॉपी करके इनिशियलाइज़ करने के लिए बुलाया जाए , और फिर इसे नष्ट किए बिना अस्थायी मान को छोड़ दें।

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


2
मूव कन्स्ट्रक्टर और कॉपी कंस्ट्रक्टर अलग कैसे हैं?
dicroce

1
@dicroce: वे वाक्य रचना द्वारा भिन्न होते हैं, एक मैट्रिक्स (const मैट्रिक्स और src) (कॉपी कंस्ट्रक्टर) की तरह दिखता है और दूसरा मैट्रिक्स (मैट्रिक्स && src) (मूव कंस्ट्रक्टर) की तरह दिखता है, एक बेहतर उदाहरण के लिए मेरे मुख्य उत्तर की जांच करें।
snk_kid

3
@dicroce: एक खाली वस्तु बनाता है, और एक प्रतिलिपि बनाता है। यदि ऑब्जेक्ट में संग्रहीत डेटा बड़ा है, तो एक कॉपी महंगी हो सकती है। उदाहरण के लिए, std :: वेक्टर।
बिली ओनेल

1
@ कुंज 2aan: यह आपके कंपाइलर पर निर्भर करता है, मुझे संदेह है। कंपाइलर फ़ंक्शन के अंदर एक अस्थायी ऑब्जेक्ट बना सकता है, और फिर इसे कॉलर के रिटर्न मान में स्थानांतरित कर सकता है। या, यह एक मूव कंस्ट्रक्टर का उपयोग करने की आवश्यकता के बिना, रिटर्न वैल्यू में सीधे ऑब्जेक्ट का निर्माण करने में सक्षम हो सकता है।
ग्रेग हेविगिल

2
: @Jichao: यह एक अनुकूलन RVO कहा जाता है, देखें अंतर के बारे में अधिक जानकारी के लिए इस सवाल यह है कि stackoverflow.com/questions/5031778/...
ग्रेग Hewgill

30

यदि आप वास्तव में चालित शब्दार्थ की एक अच्छी, गहन व्याख्या में रुचि रखते हैं, तो मैं उन पर मूल पेपर पढ़ने की अत्यधिक सलाह दूंगा, "C ++ भाषा में मूव शब्दार्थ समर्थन जोड़ने का प्रस्ताव।"

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


27

जब कोई स्रोत स्रोत की आवश्यकता नहीं है, तो उन्हें स्थानांतरित करने के बजाय स्थानांतरित शब्दार्थ संसाधनों को स्थानांतरित करने के बारे में है ।

C ++ 03 में, वस्तुओं को अक्सर कॉपी किया जाता है, केवल किसी भी कोड को फिर से मूल्य का उपयोग करने से पहले नष्ट या असाइन-ओवर करने के लिए। उदाहरण के लिए, जब आप किसी फ़ंक्शन से वैल्यू करके लौटते हैं- जब तक कि आरवीओ किक नहीं करता है - आप जिस वैल्यू पर लौट रहे हैं, उसे कॉलर के स्टैक फ्रेम में कॉपी किया जाता है, और फिर यह स्कोप से बाहर चला जाता है और नष्ट हो जाता है। यह कई उदाहरणों में से एक है: जब स्रोत ऑब्जेक्ट अस्थायी होता है, तो पास-दर-मूल्य देखें, जैसे sortकि एल्गोरिदम बस आइटमों को पुनर्व्यवस्थित करता है, vectorजब इसके capacity()पार हो जाता है , तो वास्तविककरण आदि।

जब इस तरह की कॉपी / जोड़ी नष्ट होती है, तो यह आम तौर पर होता है, क्योंकि वस्तु कुछ भारी संसाधन का मालिक है। उदाहरण के लिए, vector<string>एक गतिशील रूप से आवंटित मेमोरी ब्लॉक में stringऑब्जेक्ट्स की एक सरणी हो सकती है, प्रत्येक अपनी गतिशील मेमोरी के साथ। ऐसी वस्तु की नकल करना महंगा है: आपको स्रोत में प्रत्येक गतिशील रूप से आवंटित ब्लॉकों के लिए नई मेमोरी आवंटित करनी होगी, और सभी मूल्यों को कॉपी करना होगा। फिर आपको वह सारी मेमोरी डीललेट करने की जरूरत है जो आपने अभी कॉपी की है। हालांकि, चलती एक बड़े vector<string>साधन बस गंतव्य के लिए कुछ संकेत दिए गए (कि गतिशील स्मृति ब्लॉक को देखें) को कॉपी करने और स्रोत में उन्हें बाहर के शून्यीकरण।


23

आसान (व्यावहारिक) शब्दों में:

किसी ऑब्जेक्ट को कॉपी करने का अर्थ है उसके "स्थिर" सदस्यों को कॉपी करना और newऑपरेटर को उसकी गतिशील वस्तुओं के लिए कॉल करना । सही?

class A
{
   int i, *p;

public:
   A(const A& a) : i(a.i), p(new int(*a.p)) {}
   ~A() { delete p; }
};

हालाँकि, किसी ऑब्जेक्ट को स्थानांतरित करने के लिए (मैं दोहराता हूं, एक व्यावहारिक दृष्टिकोण में) का तात्पर्य केवल गतिशील वस्तुओं के पॉइंटर्स को कॉपी करने के लिए है, न कि नए बनाने के लिए।

लेकिन, क्या यह खतरनाक नहीं है? बेशक, आप एक गतिशील वस्तु को दो बार नष्ट कर सकते हैं (विभाजन दोष)। इसलिए, इससे बचने के लिए, आपको स्रोत बिंदुओं को दो बार नष्ट करने से बचने के लिए "अमान्य" करना चाहिए:

class A
{
   int i, *p;

public:
   // Movement of an object inside a copy constructor.
   A(const A& a) : i(a.i), p(a.p)
   {
     a.p = nullptr; // pointer invalidated.
   }

   ~A() { delete p; }
   // Deleting NULL, 0 or nullptr (address 0x0) is safe. 
};

ठीक है, लेकिन अगर मैं एक वस्तु को स्थानांतरित करता हूं, तो स्रोत वस्तु बेकार हो जाती है, नहीं? बेशक, लेकिन कुछ स्थितियों में यह बहुत उपयोगी है। सबसे स्पष्ट एक तब होता है जब मैं एक फ़ंक्शन को एक अनाम वस्तु (टेम्पोरल, रवल्यू ऑब्जेक्ट, ..., आप इसे विभिन्न नामों से बुला सकता हूं) के साथ कॉल करता हूं:

void heavyFunction(HeavyType());

उस स्थिति में, एक अनाम ऑब्जेक्ट बनाया जाता है, जिसे फ़ंक्शन पैरामीटर में कॉपी किया जाता है, और बाद में हटा दिया जाता है। इसलिए, यहां ऑब्जेक्ट को स्थानांतरित करना बेहतर है, क्योंकि आपको अनाम ऑब्जेक्ट की आवश्यकता नहीं है और आप समय और मेमोरी को बचा सकते हैं।

यह एक "प्रतिद्वंद्विता" संदर्भ की अवधारणा की ओर जाता है। C ++ 11 में वे केवल यह पता लगाने के लिए मौजूद हैं कि प्राप्त वस्तु अनाम है या नहीं। मुझे लगता है कि आप पहले से ही जानते हैं कि एक "लैवल्यू" एक असाइन करने योग्य इकाई ( =ऑपरेटर के बाएं भाग ) है, इसलिए आपको एक लैवल्यू के रूप में कार्य करने के लिए सक्षम होने के लिए किसी ऑब्जेक्ट का नामांकित संदर्भ चाहिए। एक प्रतिद्वंद्विता इसके ठीक विपरीत है, एक वस्तु जिसका कोई संदर्भ नहीं है। उसकी वजह से, अनाम वस्तु और लकीरें पर्यायवाची हैं। इसलिए:

class A
{
   int i, *p;

public:
   // Copy
   A(const A& a) : i(a.i), p(new int(*a.p)) {}

   // Movement (&& means "rvalue reference to")
   A(A&& a) : i(a.i), p(a.p)
   {
      a.p = nullptr;
   }

   ~A() { delete p; }
};

इस मामले में, जब किसी प्रकार की वस्तु को A"कॉपी" किया जाना चाहिए, तो संकलक एक लवल्यू संदर्भ या एक रवेउल रेफरेंस बनाता है यदि पारित वस्तु का नाम है या नहीं। जब नहीं, तो आपके मूव-कंस्ट्रक्टर को कॉल किया जाता है और आपको पता होता है कि ऑब्जेक्ट टेम्पोरल है और आप उसकी डायनामिक ऑब्जेक्ट्स को कॉपी करने के बजाय स्पेस और मेमोरी सेव कर सकते हैं।

यह याद रखना महत्वपूर्ण है कि "स्थिर" वस्तुओं को हमेशा कॉपी किया जाता है। एक स्थिर वस्तु (ढेर में वस्तु और ढेर पर नहीं) को "स्थानांतरित" करने का कोई तरीका नहीं है। इसलिए, जब किसी वस्तु का कोई गतिशील सदस्य (प्रत्यक्ष या अप्रत्यक्ष रूप से) अप्रासंगिक होता है, तो "मूव" / "कॉपी" होता है।

यदि आपकी वस्तु जटिल है और विध्वंसक के अन्य माध्यमिक प्रभाव हैं, जैसे किसी पुस्तकालय के कार्य को कॉल करना, अन्य वैश्विक कार्यों को कॉल करना या जो कुछ भी है, शायद एक ध्वज के साथ एक आंदोलन को संकेत देना बेहतर है:

class Heavy
{
   bool b_moved;
   // staff

public:
   A(const A& a) { /* definition */ }
   A(A&& a) : // initialization list
   {
      a.b_moved = true;
   }

   ~A() { if (!b_moved) /* destruct object */ }
};

तो, आपका कोड कम है (आपको nullptrप्रत्येक गतिशील सदस्य के लिए असाइनमेंट करने की आवश्यकता नहीं है ) और अधिक सामान्य।

अन्य विशिष्ट प्रश्न: क्या बीच का अंतर है A&&और const A&&? बेशक, पहले मामले में, आप ऑब्जेक्ट को संशोधित कर सकते हैं और दूसरे में नहीं, लेकिन, व्यावहारिक अर्थ? दूसरे मामले में, आप इसे संशोधित नहीं कर सकते हैं, इसलिए आपके पास ऑब्जेक्ट को अमान्य करने के लिए कोई तरीका नहीं है (केवल एक परिवर्तनशील ध्वज या ऐसा कुछ) को छोड़कर, और कॉपी निर्माता के लिए कोई व्यावहारिक अंतर नहीं है।

और क्या सही अग्रेषण है ? यह जानना महत्वपूर्ण है कि "कॉलर का दायरा" एक नामित वस्तु का संदर्भ है। लेकिन वास्तविक दायरे में, एक रेवल्यू रेफरेंस एक ऑब्जेक्ट का नाम है, इसलिए, यह एक नामित ऑब्जेक्ट के रूप में कार्य करता है। यदि आप किसी अन्य फ़ंक्शन के लिए एक रेवल्यू संदर्भ पास करते हैं, तो आप एक नामित ऑब्जेक्ट पास कर रहे हैं, इसलिए, ऑब्जेक्ट को टेम्पोरल ऑब्जेक्ट की तरह प्राप्त नहीं किया जाता है।

void some_function(A&& a)
{
   other_function(a);
}

ऑब्जेक्ट aको वास्तविक पैरामीटर पर कॉपी किया जाएगा other_function। यदि आप चाहते हैं कि ऑब्जेक्ट aको अस्थायी ऑब्जेक्ट के रूप में माना जाता रहे, तो आपको std::moveफ़ंक्शन का उपयोग करना चाहिए :

other_function(std::move(a));

इस रेखा के साथ, एक प्रतिद्वंद्विता में std::moveडाली जाएगी aऔर other_functionएक अनाम वस्तु के रूप में वस्तु प्राप्त करेगी। बेशक, यदि other_functionअनाम वस्तुओं के साथ काम करने के लिए विशिष्ट अधिभार नहीं है , तो यह अंतर महत्वपूर्ण नहीं है।

क्या यह एकदम सही है? नहीं, लेकिन हम बहुत करीब हैं। पूर्ण अग्रेषण केवल टेम्प्लेट के साथ काम करने के लिए उपयोगी है, कहने के उद्देश्य से: यदि मुझे किसी अन्य फ़ंक्शन को ऑब्जेक्ट पास करने की आवश्यकता है, तो मुझे आवश्यकता है कि अगर मुझे एक नामित ऑब्जेक्ट प्राप्त होता है, तो ऑब्जेक्ट को नामित ऑब्जेक्ट के रूप में पारित किया जाता है, और जब नहीं, मैं इसे एक अनाम वस्तु की तरह पास करना चाहता हूं:

template<typename T>
void some_function(T&& a)
{
   other_function(std::forward<T>(a));
}

यह एक प्रोटोटाइप फ़ंक्शन का हस्ताक्षर है जो कि C के माध्यम से C ++ 11 में कार्यान्वित, सही अग्रेषण का उपयोग करता है std::forward। यह फ़ंक्शन टेम्प्लेट इंस्टेंटेशन के कुछ नियमों का फायदा उठाता है:

 `A& && == A&`
 `A&& && == A&&`

तो, अगर ( T = A &), भी ( A & && => A &) के Tलिए एक संदर्भ है । यदि एक सन्दर्भ संदर्भ है , तो भी (A && && => A &&)। दोनों ही मामलों में, वास्तविक दायरे में एक नामित वस्तु है, लेकिन कॉलर के दृष्टिकोण से इसके "संदर्भ प्रकार" की जानकारी शामिल है। यह जानकारी ( ) टेम्पलेट पैरामीटर के रूप में उत्तीर्ण की जाती है और 'ए' के ​​प्रकार के अनुसार ले जाया जाता है या नहीं ।AaTAaaTTforwardT


20

यह कॉपी शब्दार्थों की तरह है, लेकिन इसके बजाय आपको डेटा के सभी डुप्लिकेट करने के लिए ऑब्जेक्ट को "स्थानांतरित" होने से डेटा चोरी करना है।


13

तुम्हें पता है कि एक कॉपी शब्दार्थ सही क्या है? इसका मतलब है कि आपके पास ऐसे प्रकार हैं जो कॉपी करने योग्य हैं, उपयोगकर्ता द्वारा परिभाषित किए गए प्रकारों के लिए आप इसे परिभाषित करते हैं या तो स्पष्ट रूप से कॉपी निर्माता और असाइनमेंट ऑपरेटर लिख रहे हैं या संकलक उन्हें अंतर्निहित रूप से उत्पन्न करता है। यह एक कॉपी करेगा।

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

आप ऐसा कब करना चाहेंगे? well std :: वेक्टर एक उदाहरण है, मान लीजिए कि आपने एक अस्थायी std :: वेक्टर बनाया है और आप इसे किसी फ़ंक्शन से लौटाते हैं:

std::vector<foo> get_foos();

जब फ़ंक्शन वापस लौटता है, तो आप कॉपी कंस्ट्रक्टर से ओवरहेड करने जा रहे हैं, अगर (और यह C ++ 0x में होगा) std :: वेक्टर में एक मूव कंस्ट्रक्टर है जिसे कॉपी करने के बजाय इसे केवल सेट किया जा सकता है और गतिशील रूप से आवंटित किया गया है। नए उदाहरण के लिए स्मृति। यह std :: auto_ptr के साथ स्थानांतरण-के-स्वामित्व शब्दार्थ की तरह है।


1
मुझे नहीं लगता कि यह एक महान उदाहरण है, क्योंकि इन फ़ंक्शन रिटर्न वैल्यू उदाहरणों में रिटर्न वैल्यू ऑप्टिमाइज़ेशन शायद कॉपी ऑपरेशन को पहले ही समाप्त कर रहा है।
ज़ेन लिंक्स

7

चाल शब्दार्थ की आवश्यकता को स्पष्ट करने के लिए , चलिए इस उदाहरण पर चलते हैं बिना शब्दार्थ के:

यहाँ एक फ़ंक्शन है जो एक प्रकार की वस्तु लेता है Tऔर उसी प्रकार का ऑब्जेक्ट लौटाता है T:

T f(T o) { return o; }
  //^^^ new object constructed

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

T b = f(a);
  //^ new object constructed

दो नई वस्तुओं का निर्माण किया गया है, जिनमें से एक अस्थायी वस्तु है जो केवल फ़ंक्शन की अवधि के लिए उपयोग की जाती है।

जब नए ऑब्जेक्ट को रिटर्न वैल्यू से बनाया जाता है, तो कॉपी कंस्ट्रक्टर को अस्थायी ऑब्जेक्ट की सामग्री को नए ऑब्जेक्ट b में कॉपी करने के लिए बुलाया जाता है । फ़ंक्शन पूरा होने के बाद, फ़ंक्शन में उपयोग की जाने वाली अस्थायी वस्तु दायरे से बाहर हो जाती है और नष्ट हो जाती है।


अब, आइए विचार करें कि एक कॉपी कंस्ट्रक्टर क्या करता है।

इसे पहले ऑब्जेक्ट को इनिशियलाइज़ करना होगा, फिर पुराने ऑब्जेक्ट से नए के लिए सभी प्रासंगिक डेटा को कॉपी करना होगा।
वर्ग के आधार पर, शायद इसके कंटेनर में बहुत अधिक डेटा है, फिर यह बहुत समय और स्मृति उपयोग का प्रतिनिधित्व कर सकता है

// Copy constructor
T::T(T &old) {
    copy_data(m_a, old.m_a);
    copy_data(m_b, old.m_b);
    copy_data(m_c, old.m_c);
}

मूव सिमेंटिक्स के साथ अब यह संभव है कि इस काम को कम से कम किया जा सके ताकि नकल के बजाय डेटा को स्थानांतरित किया जा सके।

// Move constructor
T::T(T &&old) noexcept {
    m_a = std::move(old.m_a);
    m_b = std::move(old.m_b);
    m_c = std::move(old.m_c);
}

डेटा को स्थानांतरित करने में नई वस्तु के साथ डेटा को फिर से जोड़ना शामिल है। और कोई भी नकल बिल्कुल नहीं होती है।

यह एक rvalueसंदर्भ के साथ पूरा किया है ।
एक rvalueसंदर्भ lvalueएक महत्वपूर्ण अंतर के साथ एक संदर्भ की तरह बहुत काम करता है :
एक संदर्भ संदर्भ को स्थानांतरित किया जा सकता है और एक अंतराल नहीं हो सकता।

से cppreference.com :

मजबूत अपवाद गारंटी को संभव बनाने के लिए, उपयोगकर्ता-परिभाषित चाल निर्माणकर्ताओं को अपवाद नहीं फेंकना चाहिए। वास्तव में, मानक कंटेनर आमतौर पर std :: Move_if_noexcept पर निर्भर करते हैं कि कंटेनर तत्वों को स्थानांतरित करने की आवश्यकता होने पर चाल और कॉपी के बीच चयन करें। यदि दोनों कॉपी और मूव कंस्ट्रक्टर प्रदान किए जाते हैं, तो ओवरलोड रेजोल्यूशन मूव कंस्ट्रक्टर का चयन करता है, यदि तर्क एक प्रतिद्वंद्विता है (या तो कोई अस्थायी जैसे कि एक अस्थायी या xvalue जैसे कि std का परिणाम :: चाल), और कॉपी कंस्ट्रक्टर का चयन करता है यदि तर्क एक लवल्यू है (नामित वस्तु या एक फ़ंक्शन / ऑपरेटर जो लैवल्यू संदर्भ देता है)। यदि केवल कॉपी कंस्ट्रक्टर प्रदान किया जाता है, तो सभी तर्क श्रेणियां इसे चुनती हैं (जब तक कि यह कॉन्स्ट का संदर्भ ले लेती है, क्योंकि रिवाल्स कॉन्स्ट रेफरेंस से बंध सकते हैं), जो चलते समय अनुपलब्ध होने पर फॉलबैक की प्रतिलिपि बनाता है। कई स्थितियों में, मूव कन्स्ट्रक्टर्स को ऑप्टिमाइज़ किया जाता है, भले ही वे ऑब्जर्वेबल साइड-इफेक्ट्स पैदा करते हों, कॉपी एलिजन देखें। एक निर्माणकर्ता को 'मूव कंस्ट्रक्टर' कहा जाता है, जब यह एक पैरामीटर के रूप में एक रव्यू संदर्भ लेता है। यह कुछ भी स्थानांतरित करने के लिए बाध्य नहीं है, कक्षा को स्थानांतरित करने के लिए संसाधन की आवश्यकता नहीं है और एक 'मूव कंस्ट्रक्टर' संसाधन को स्वीकार्य (लेकिन शायद समझदार नहीं) मामले में स्थानांतरित करने में सक्षम नहीं हो सकता है, जहां पैरामीटर एक है const rvalue सन्दर्भ (const T &&)।


7

मैं यह सुनिश्चित करने के लिए लिख रहा हूं कि मैं इसे ठीक से समझूं।

बड़ी वस्तुओं की अनावश्यक नकल से बचने के लिए मूव सिमेंटिक्स बनाए गए। अपनी पुस्तक "द सी ++ प्रोग्रामिंग लैंग्वेज" में बज़्ने स्ट्रॉस्ट्रुप दो उदाहरणों का उपयोग करता है जहां डिफ़ॉल्ट रूप से अनावश्यक नकल होती है: एक, दो बड़ी वस्तुओं की अदला-बदली, और दो, एक विधि से बड़ी वस्तु की वापसी।

दो बड़ी वस्तुओं की अदला-बदली में आमतौर पर पहली वस्तु को अस्थायी वस्तु में कॉपी करना, दूसरी वस्तु को पहली वस्तु में कॉपी करना और दूसरी वस्तु में अस्थायी वस्तु की नकल करना शामिल होता है। एक अंतर्निहित प्रकार के लिए, यह बहुत तेज़ है, लेकिन बड़ी वस्तुओं के लिए इन तीन प्रतियों में बड़ी मात्रा में समय लग सकता है। एक "चाल असाइनमेंट" प्रोग्रामर को डिफ़ॉल्ट कॉपी व्यवहार को ओवरराइड करने की अनुमति देता है और इसके बजाय वस्तुओं के संदर्भ स्वैप करता है, जिसका अर्थ है कि कोई भी नकल नहीं है और स्वैप ऑपरेशन बहुत तेज है। इस कदम असाइनमेंट को std :: move () विधि से कॉल करके लागू किया जा सकता है।

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

ऐसी भाषाओं में जो स्थानीय वस्तुओं (यानी, स्टैक पर मौजूद वस्तुओं) के निर्माण की अनुमति नहीं देते हैं, इस प्रकार की समस्याएं नहीं होती हैं क्योंकि सभी ऑब्जेक्ट्स को ढेर पर आवंटित किया जाता है और हमेशा संदर्भ द्वारा एक्सेस किया जाता है।


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

आप swap()बिना गति के शब्दार्थ लिख सकते हैं । "कदम असाइनमेंट को std :: move () विधि से कॉल करके लागू किया जा सकता है।" - यह कभी-कभी उपयोग करने के लिए आवश्यक है std::move()- हालांकि यह वास्तव में कुछ भी नहीं ले जाता है - बस कंपाइलर को पता चल जाता है कि तर्क चल रहा है, कभी-कभी std::forward<>()(संदर्भों को अग्रेषित करने के साथ), और अन्य समय पर संकलक जानता है कि एक मूल्य स्थानांतरित किया जा सकता है।
टोनी डेलरो

-2

यहाँ एक जवाब किताब 'सी ++ प्रोग्रामिंग भाषा "Bjarne Stroustrup द्वारा से। यदि आप वीडियो नहीं देखना चाहते हैं, तो आप नीचे दिया गया पाठ देख सकते हैं:

इस स्निपेट पर विचार करें। ऑपरेटर + से लौटने पर परिणाम को स्थानीय चर से resऔर किसी ऐसे स्थान पर कॉपी करना शामिल होता है, जहाँ कॉलर इसे एक्सेस कर सकता है।

Vector operator+(const Vector& a, const Vector& b)
{
    if (a.size()!=b.size())
        throw Vector_siz e_mismatch{};
    Vector res(a.size());
        for (int i=0; i!=a.size(); ++i)
            res[i]=a[i]+b[i];
    return res;
}

हम वास्तव में एक प्रति नहीं चाहते थे; हम केवल एक परिणाम से बाहर निकलना चाहते थे। इसलिए हमें इसे कॉपी करने के बजाय वेक्टर को स्थानांतरित करने की आवश्यकता है। हम निम्नानुसार कंस्ट्रक्टर को परिभाषित कर सकते हैं:

class Vector {
    // ...
    Vector(const Vector& a); // copy constructor
    Vector& operator=(const Vector& a); // copy assignment
    Vector(Vector&& a); // move constructor
    Vector& operator=(Vector&& a); // move assignment
};

Vector::Vector(Vector&& a)
    :elem{a.elem}, // "grab the elements" from a
    sz{a.sz}
{
    a.elem = nullptr; // now a has no elements
    a.sz = 0;
}

The && का अर्थ "संदर्भ संदर्भ" है और यह एक संदर्भ है जिससे हम एक अंतराल बाँध सकते हैं। "रेवल्यू" का उद्देश्य "लैवल्यू" को पूरक करना है जिसका मोटे तौर पर अर्थ है "कुछ ऐसा जो एक असाइनमेंट के बाईं ओर दिखाई दे सकता है।" तो एक प्रतिद्वंद्विता का अर्थ है मोटे तौर पर "एक मूल्य जिसे आप असाइन नहीं कर सकते हैं", जैसे कि एक फ़ंक्शन कॉल द्वारा लौटा हुआ पूर्णांक, और resवैक्टर के लिए ऑपरेटर + () में स्थानीय चर।

अब, बयान return res;की नकल नहीं होगी!

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.