हम नकल क्यों करते हैं?


98

मैंने कहीं कोड देखा, जिसमें किसी ने किसी ऑब्जेक्ट को कॉपी करने का फैसला किया और बाद में इसे एक कक्षा के डेटा सदस्य के पास ले गया। इसने मुझे इस भ्रम में छोड़ दिया कि मुझे लगा कि नकल करने से बचने के लिए आगे बढ़ने का पूरा बिंदु था। यहाँ उदाहरण है:

struct S
{
    S(std::string str) : data(std::move(str))
    {}
};

यहाँ मेरे सवाल हैं:

  • हम एक संदर्भ-संदर्भ क्यों नहीं ले रहे हैं str?
  • क्या एक कॉपी महंगी नहीं होगी, विशेष रूप से कुछ दिया std::string?
  • लेखक को कॉपी बनाने का फैसला करने के लिए क्या कारण होगा?
  • मुझे यह कब करना चाहिए?

मेरे लिए एक मूर्खतापूर्ण गलती की तरह लग रहा है, लेकिन मुझे यह देखने में दिलचस्पी होगी कि क्या विषय पर अधिक ज्ञान वाले किसी व्यक्ति को इसके बारे में कुछ भी कहना है।
डेव



जवाबों:


97

इससे पहले कि मैं आपके प्रश्नों का उत्तर दूं, एक बात जो आपको गलत लग रही है: C ++ 11 में मान लेने का मतलब हमेशा नकल करना नहीं होता है। यदि एक प्रतिद्वंद्विता पारित हो जाती है, तो उसे स्थानांतरित किया जाएगा (बशर्ते एक व्यवहार्य चाल निर्माणकर्ता मौजूद है) नकल करने के बजाय। और std::stringएक मूव कंस्ट्रक्टर है।

C ++ 03 के विपरीत, C ++ 11 में अक्सर मानों को मानदंड लेना मुहावरेदार है, जिन कारणों के लिए मैं नीचे बताने जा रहा हूं। मापदंडों को स्वीकार करने के बारे में दिशानिर्देशों के अधिक सामान्य सेट के लिए StackOverflow पर यह प्रश्नोत्तर देखें ।

हम एक संदर्भ-संदर्भ क्यों नहीं ले रहे हैं str?

क्योंकि इससे अंतराल को पास करना असंभव हो जाएगा, जैसे कि:

std::string s = "Hello";
S obj(s); // s is an lvalue, this won't compile!

यदि Sकेवल एक कंस्ट्रक्टर था जो प्रतिद्वंद्वियों को स्वीकार करता है, तो उपरोक्त संकलन नहीं होगा।

क्या एक कॉपी महंगी नहीं होगी, विशेष रूप से कुछ दिया std::string?

यदि आप एक प्रतिद्वंद्विता पास करते हैं , तो इसे स्थानांतरित कर दिया जाएगा str, और अंत में इसे स्थानांतरित कर दिया जाएगा data। कोई भी नकल नहीं करेगा। यदि आप एक लेवल्यू पास करते हैं, तो दूसरी तरफ, उस लैवल्यू को कॉपी किया जाएगा str, और फिर उसमें स्थानांतरित किया जाएगा data

तो इसे योग करने के लिए, दो चालों के लिए, एक प्रति और एक चाल के लिए lvalues।

लेखक को कॉपी बनाने का फैसला करने के लिए क्या कारण होगा?

सबसे पहले, जैसा कि मैंने ऊपर उल्लेख किया है, पहले वाला हमेशा एक प्रति नहीं है; और इसने कहा, इसका उत्तर है: " क्योंकि यह कुशल है ( std::stringवस्तुओं की चाल सस्ती है) और सरल है "।

इस धारणा के तहत कि चालें सस्ती हैं (एसएसओ की अनदेखी यहां), वे इस डिजाइन की समग्र दक्षता पर विचार करते समय व्यावहारिक रूप से अवहेलना कर सकते हैं। यदि हम ऐसा करते हैं, तो हमारे पास lvalues ​​के लिए एक प्रति है (जैसा कि अगर हम एक lvalue संदर्भ को स्वीकार करते हैं तो const) और rvalues ​​के लिए कोई प्रतिलिपि नहीं है (जबकि हमारे पास अभी भी एक प्रतिलिपि होगी यदि हमने एक lvalue संदर्भ स्वीकार किया है const)।

इसका मतलब यह है कि मूल्य द्वारा लेना उतना ही अच्छा है जितना constकि अंतराल के संदर्भ में लिया जाता है, जब अंतराल प्रदान किए जाते हैं, और जब प्रतिद्वंद्विता प्रदान की जाती है तो बेहतर होता है।

पुनश्च: कुछ संदर्भ प्रदान करने के लिए, मेरा मानना ​​है कि यह क्यू एंड ए ओपी का उल्लेख है।


2
यह उल्लेख करने के लायक है कि यह C ++ 11 पैटर्न है जो const T&तर्क पास करने की जगह लेता है: सबसे खराब स्थिति (अंतराल) में यह समान है, लेकिन एक अस्थायी के मामले में आपको केवल अस्थायी स्थानांतरित करना होगा। फायदे का सौदा।
श्याम

3
@ user2030677: जब तक आप एक संदर्भ संग्रहीत नहीं कर रहे हैं, तब तक उस प्रतिलिपि के आसपास कोई नहीं मिल रहा है।
बेंजामिन लिंडले

5
@ user2030677: कौन परवाह करता है कि कॉपी कितनी महंगी है जब तक आपको इसकी आवश्यकता है (और आप करते हैं, यदि आप अपने सदस्य में कॉपी रखना चाहते हैं data)? आपके पास एक प्रति होगी, भले ही आप लेवल्यू संदर्भ द्वारा लेंconst
एंडी प्रोल

3
@BenjaminLindley: प्रारंभिक के रूप में, मैंने लिखा: " इस धारणा के तहत कि चालें सस्ती हैं, वे इस डिजाइन की समग्र दक्षता पर विचार करते समय व्यावहारिक रूप से अवहेलना कर सकते हैं। " तो हाँ, वहाँ एक कदम के उपरिव्यय होगा, लेकिन इसे तब तक नगण्य माना जाना चाहिए जब तक कि इस बात का सबूत न हो कि यह एक वास्तविक चिंता है जो सरल डिजाइन को कुछ अधिक कुशल में बदलने को सही ठहराती है।
एंडी प्रोल

1
@ user2030677: लेकिन यह एक पूरी तरह से अलग उदाहरण है। अपने प्रश्न से उदाहरण में आप हमेशा एक प्रति धारण करते हैं data!
एंडी प्रोल

51

यह समझने के लिए कि यह एक अच्छा पैटर्न क्यों है, हमें C ++ 03 और C ++ 11 दोनों में विकल्पों की जांच करनी चाहिए।

हमारे पास लेने की C ++ 03 विधि है std::string const&:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str)
  {}
};

इस मामले में, हमेशा एक ही प्रति प्रदर्शित की जाएगी। यदि आप कच्चे सी स्ट्रिंग std::stringसे निर्माण करते हैं, तो एक निर्माण किया जाएगा, फिर दोबारा कॉपी किया जाएगा: दो आवंटन।

एक संदर्भ लेने के लिए C ++ 03 विधि है std::string, फिर इसे स्थानीय में स्वैप करना है std::string:

struct S
{
  std::string data; 
  S(std::string& str)
  {
    std::swap(data, str);
  }
};

"चाल शब्दार्थ" का C ++ 03 संस्करण है, और swapअक्सर ऐसा करने के लिए बहुत सस्ता होने के लिए अनुकूलित किया जा सकता है (बहुत पसंद है move)। इसका संदर्भ में विश्लेषण भी किया जाना चाहिए:

S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal

और आपको एक गैर-अस्थायी बनाने के लिए मजबूर करता है std::string, फिर इसे त्याग दें। (एक अस्थायी std::stringएक गैर-कास्ट संदर्भ के लिए बाध्य नहीं कर सकते हैं)। हालाँकि, केवल एक ही आवंटन किया जाता है। C ++ 11 संस्करण को एक की &&आवश्यकता होगी और आपको इसे std::moveअस्थायी रूप से या इसके साथ कॉल करने की आवश्यकता होगी : इसके लिए कॉल करने वाले को स्पष्ट रूप से कॉल के बाहर एक कॉपी बनाता है, और उस कॉपी को फ़ंक्शन या कंस्ट्रक्टर में स्थानांतरित करना होगा।

struct S
{
  std::string data; 
  S(std::string&& str): data(std::move(str))
  {}
};

उपयोग:

S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal

अगला, हम पूर्ण C ++ 11 संस्करण कर सकते हैं, जो प्रतिलिपि और move:

struct S
{
  std::string data; 
  S(std::string const& str) : data(str) {} // lvalue const, copy
  S(std::string && str) : data(std::move(str)) {} // rvalue, move
};

हम तब जांच कर सकते हैं कि इसका उपयोग कैसे किया जाता है:

S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data

std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data

std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data

यह बहुत स्पष्ट है कि यह 2 अधिभार तकनीक कम से कम कुशल है, यदि ऐसा नहीं है, तो उपरोक्त दो सी ++ 03 शैलियों की तुलना में। मैं इस 2-अधिभार संस्करण को "सबसे इष्टतम" संस्करण दूंगा।

अब, हम टेक-बाय-कॉपी संस्करण की जाँच करेंगे:

struct S2 {
  std::string data;
  S2( std::string arg ):data(std::move(x)) {}
};

उन परिदृश्यों में से प्रत्येक में:

S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data

std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data

std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data

यदि आप इस साइड-बाय-साइड की "सबसे इष्टतम" संस्करण के साथ तुलना करते हैं, तो हम बिल्कुल एक अतिरिक्त करते हैं move! एक बार नहीं हम एक अतिरिक्त करते हैं copy

इसलिए अगर हम मानते हैं कि moveयह सस्ता है, तो यह संस्करण हमें लगभग सबसे इष्टतम संस्करण के समान प्रदर्शन देता है, लेकिन 2 गुना कम कोड।

और यदि आप 2 से 10 तर्क कह रहे हैं, तो कोड में कमी घातीय है - 1 तर्क के साथ 2x गुणा कम, 2 के साथ 4x, 3 के साथ 8x, 4 तर्क के साथ 16x।

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

और बहुत सारे उत्पन्न कार्यों का अर्थ बड़ा निष्पादन योग्य कोड आकार है, जो स्वयं प्रदर्शन को कम कर सकता है।

कुछ moveएस की लागत के लिए , हमें कम कोड और लगभग समान प्रदर्शन मिलता है, और अक्सर कोड को समझना आसान होता है।

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

'टेक बाय वैल्यू' तकनीक का एक और फायदा यह है कि अक्सर मूव करने वाले कंस्ट्रक्टर्स नोएसेप्ट होते हैं। इसका मतलब है कि जो काम बाय-वैल्यू लेते हैं और अपने तर्क से बाहर निकलते हैं, वे अक्सर नॉइसेप्ट हो सकते हैं, जो किसी भी throwएस को अपने शरीर से बाहर निकालकर कॉलिंग स्कोप में ले जाते हैं। (जो कभी-कभी प्रत्यक्ष निर्माण के माध्यम से इसे टाल सकते हैं, या वस्तुओं का निर्माण कर सकते हैं और moveतर्क में, यह नियंत्रित कर सकते हैं कि फेंकना कहां होता है)। तरीकों को बनाना अक्सर इसके लायक होता है।


मैं यह भी जोड़ूंगा कि अगर हम जानते हैं कि हम एक प्रतिलिपि बनाएंगे, तो हमें कंपाइलर को इसे करने देना चाहिए, क्योंकि कंपाइलर हमेशा बेहतर जानता है।
रेयानरी

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

आपको 3 अधिभार तकनीक में "लैवल्यू नॉन-कास्ट, कॉपी" संस्करण की आवश्यकता क्यों है? क्या "लैवल्यू कॉस्ट, कॉपी" भी नॉन कॉस्ट केस को हैंडल नहीं करता है?
ब्रूनो मार्टिनेज

@BrunoMartinez हम नहीं!
यक्क - एडम नेवरामोंट

13

यह शायद इरादतन है और मुहावरे की नकल और अदला-बदली के समान है । मूल रूप से जब स्ट्रिंग को कंस्ट्रक्टर से पहले कॉपी किया जाता है, तो कंस्ट्रक्टर स्वयं अपवाद सुरक्षित होता है क्योंकि यह केवल अस्थायी स्ट्रिंग को स्वैप (मूव) करता है।


कॉपी-और-स्वैप समानांतर के लिए +1। वास्तव में इसमें बहुत सारी समानताएं हैं।
श्याम

11

आप मूव के लिए कंस्ट्रक्टर और कॉपी के लिए एक लिखकर खुद को दोहराना नहीं चाहते:

S(std::string&& str) : data(std::move(str)) {}
S(const std::string& str) : data(str) {}

यह बहुत बॉयलरप्लेट कोड है, खासकर यदि आपके पास कई तर्क हैं। आपका समाधान अनावश्यक चाल की लागत पर उस दोहराव से बचता है। (हालांकि चालन ऑपरेशन काफी सस्ता होना चाहिए, हालांकि)

प्रतिस्पर्धी मुहावरे का उपयोग सही अग्रेषण है:

template <typename T>
S(T&& str) : data(std::forward<T>(str)) {}

टेम्प्लेट मैजिक आपके द्वारा पास किए जाने वाले पैरामीटर के आधार पर मूव या कॉपी करना पसंद करेगा। यह मूल रूप से पहले संस्करण में फैलता है, जहां दोनों कंस्ट्रक्टर हाथ से लिखे गए थे। पृष्ठभूमि की जानकारी के लिए, सार्वभौमिक संदर्भों पर स्कॉट मेयर की पोस्ट देखें ।

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

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