C ++ में मूव कंस्ट्रक्टर्स का प्रेरणा और उपयोग


17

मैं हाल ही में C ++ में मूव कंस्ट्रक्टर्स के बारे में पढ़ रहा हूं (उदाहरण के लिए यहां देखें ) और मैं यह समझने की कोशिश कर रहा हूं कि वे कैसे काम करते हैं और मुझे उनका उपयोग कब करना चाहिए।

जहां तक ​​मैं समझता हूं, बड़ी वस्तुओं की नकल के कारण प्रदर्शन समस्याओं को कम करने के लिए एक चाल निर्माणकर्ता का उपयोग किया जाता है। विकिपीडिया पृष्ठ कहता है: "C ++ 03 के साथ एक पुरानी प्रदर्शन समस्या महंगी और अनावश्यक गहरी प्रतियां है जो ऑब्जेक्ट द्वारा मूल्य पारित किए जाने पर निहित हो सकती हैं।"

मैं आम तौर पर ऐसी स्थितियों को संबोधित करता हूं

  • संदर्भ द्वारा वस्तुओं को पास करके, या
  • ऑब्जेक्ट के चारों ओर से गुजरने के लिए स्मार्ट पॉइंटर्स (जैसे बूस्ट :: शेयर_प्ट्र) का उपयोग करके (स्मार्ट पॉइंटर्स ऑब्जेक्ट के बजाय कॉपी हो जाते हैं)।

ऐसी कौन सी परिस्थितियाँ हैं जिनमें उपरोक्त दोनों तकनीकें पर्याप्त नहीं हैं और एक चाल निर्माणकर्ता का उपयोग करना अधिक सुविधाजनक है?


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

जवाबों:


16

हटो शब्दार्थ सी + + के लिए एक पूरे आयाम का परिचय देते हैं - यह सिर्फ आपको सस्ते में मूल्यों को वापस करने के लिए नहीं है।

उदाहरण के लिए, बिना चाल-शब्दार्थ std::unique_ptrकाम नहीं करता - देखो std::auto_ptr, जिसे चाल-शब्दार्थ की शुरूआत के साथ हटा दिया गया था और C ++ 17 में हटा दिया गया था। किसी संसाधन को स्थानांतरित करना कॉपी करने से बहुत अलग है। यह एक अद्वितीय वस्तु के स्वामित्व के हस्तांतरण की अनुमति देता है

उदाहरण के लिए, आइए std::unique_ptrइसे न देखें , क्योंकि यह काफी चर्चा में है। आइए, OpenGL में एक वर्टेक्स बफर ऑब्जेक्ट को देखें। एक शीर्ष बफ़र GPU पर मेमोरी का प्रतिनिधित्व करता है - इसे विशेष कार्यों का उपयोग करके आवंटित करने और निपटाए जाने की आवश्यकता होती है, संभवतः यह कितनी देर तक जीवित रह सकता है, इस पर तंग अड़चनें हैं। यह भी महत्वपूर्ण है कि केवल एक मालिक इसका उपयोग करें।

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

अब, यह एक के साथ किया जा सकता है std::shared_ptr- लेकिन इस संसाधन को साझा नहीं किया जाना है। यह साझा सूचक का उपयोग करने के लिए भ्रमित करता है। आप उपयोग कर सकते हैं std::unique_ptr, लेकिन इसके लिए अभी भी कदम शब्दार्थ की आवश्यकता है।

जाहिर है, मैंने एक कदम निर्माता को लागू नहीं किया है, लेकिन आपको यह विचार मिलता है।

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


जवाब के लिए धन्यवाद। अगर एक साझा पॉइंटर का इस्तेमाल यहां किया जाए तो क्या होगा?
जियोर्जियो

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

3
@ जियोर्जियो आप एक साझा सूचक का उपयोग कर सकते हैं, लेकिन यह शब्दार्थिक रूप से गलत होगा। एक बफर साझा करना संभव नहीं है। इसके अलावा, यह अनिवार्य रूप से आपको एक पॉइंटर को एक पॉइंटर को पास करने की अनुमति देगा (चूंकि vbo मूल रूप से GPU मेमोरी के लिए एक अद्वितीय पॉइंटर है)। आपके कोड को देखने वाला कोई व्यक्ति बाद में सोच सकता है कि 'यहाँ एक साझा सूचक क्यों है? क्या यह साझा संसाधन है? यह एक बग हो सकता है! '। मूल इरादे के रूप में जितना संभव हो उतना स्पष्ट होना बेहतर है।
अधिकतम

@ जियोर्जियो हां, वह भी आवश्यकता का हिस्सा है। जब इस मामले में 'रेंडरर' कुछ संसाधन (संभवतः जीपीयू पर नई वस्तुओं के लिए पर्याप्त मेमोरी नहीं) से निपटना चाहता है, तो मेमोरी के लिए कोई अन्य हैंडल नहीं होना चाहिए। यदि आप इसे कहीं और नहीं रखते हैं, तो इसे साझा करने के लिए एक शेयर्ड_प्ट्र का उपयोग करना काम करेगा, लेकिन जब आप कर सकते हैं तो इसे पूरी तरह से स्पष्ट क्यों न करें?
अधिकतम

@Giorgio स्पष्ट करने के एक और प्रयास के लिए मेरा संपादन देखें।
अधिकतम

5

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

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

यहां मूल विचार काफी सरल है: पर्याप्त सामग्री के साथ एक वर्ग बनाएं जिसे हम इसे कॉपी करने से बचें, यदि संभव हो तो ( std::vectorहम 32767 यादृच्छिक ints के साथ भरें)। हमारे पास एक स्पष्ट प्रतिलिपि ctor है जो हमें दिखाएगा कि कब / अगर यह कॉपी किया जाता है। ऑब्जेक्ट में यादृच्छिक मानों के साथ कुछ करने के लिए हमारे पास थोड़ा अधिक कोड है, इसलिए ऑप्टिमाइज़र (कम से कम आसानी से) कक्षा के बारे में सब कुछ खत्म नहीं करेगा क्योंकि यह कुछ भी नहीं करता है।

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

मूविंग आपको उन चीजों की एक उचित संख्या करने की अनुमति देता है जो आप उनके बिना बस (सीधे) नहीं कर सकते थे। बाहरी मर्ज सॉर्ट के "मर्ज" भाग पर विचार करें - आपके पास, 8 फाइलें हैं, जिन्हें आप एक साथ मर्ज करने जा रहे हैं। आदर्श रूप से आप उन सभी फ़ाइलों को एक में रखना चाहते हैं vector- लेकिन चूंकि vector(C ++ 03 के रूप में) तत्वों को कॉपी करने में सक्षम होना चाहिए, और ifstreamकॉपी नहीं किया जा सकता है, आप कुछ के साथ अटक गए हैं , unique_ptr/ shared_ptrया उस आदेश पर कुछ उन्हें वेक्टर में डालने में सक्षम होने के लिए। ध्यान दें कि भले ही (उदाहरण के लिए) हम reserveअंतरिक्ष में हैं, vectorइसलिए हमें यकीन है कि हमारे ifstreamएस को वास्तव में कभी भी कॉपी नहीं किया जाएगा, कंपाइलर को पता नहीं चलेगा, इसलिए कोड का संकलन नहीं होगा, जबकि हम जानते हैं कि कॉपी कंस्ट्रक्टर कभी नहीं होगा वैसे भी इस्तेमाल किया।

हालांकि यह अभी भी कॉपी नहीं किया जा सकता है, C ++ 11 में स्थानांतरित किया ifstream जा सकता है। इस मामले में, वस्तुओं को शायद कभी स्थानांतरित नहीं किया जाएगा, लेकिन यह तथ्य कि वे हो सकते हैं यदि आवश्यक हो तो संकलक को खुश रखता है, इसलिए हम अपनी ifstreamवस्तुओं को vectorसीधे, बिना किसी स्मार्ट पॉइंटर हैक के रख सकते हैं ।

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

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


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

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

4

विचार करें:

vector<string> v;

जब स्ट्रिंग्स को v में जोड़ते हैं, तो यह आवश्यकतानुसार विस्तारित हो जाएगा, और प्रत्येक पुनः प्राप्ति पर स्ट्रिंग्स को कॉपी करना होगा। चाल निर्माणकर्ताओं के साथ, यह मूल रूप से एक गैर-मुद्दा है।

बेशक, आप भी कुछ ऐसा कर सकते हैं:

vector<unique_ptr<string>> v;

लेकिन यह अच्छी तरह से काम करेगा क्योंकि std::unique_ptrऔजार निर्माणकर्ता को स्थानांतरित करता है।

उपयोग std::shared_ptrकेवल (दुर्लभ) स्थितियों में समझ में आता है जब आपने वास्तव में स्वामित्व साझा किया है।


लेकिन क्या हुआ अगर हमारे बजाय इसके stringउदाहरण हैं Fooजहां इसके 30 डेटा सदस्य हैं? unique_ptrसंस्करण अधिक कुशल नहीं हो सकता है?
वासिलिस

2

वापसी मान वे होते हैं, जहाँ मैं सबसे अधिक बार किसी प्रकार के संदर्भ के बजाय मान से गुजरना चाहता हूँ। एक बड़े पैमाने पर प्रदर्शन के दंड के बिना 'स्टैक पर' किसी वस्तु को जल्दी से वापस करने में सक्षम होना अच्छा होगा। दूसरी ओर, इसे प्राप्त करना विशेष रूप से मुश्किल नहीं है (साझा संकेत केवल उपयोग करने के लिए बहुत आसान हैं ...), इसलिए मुझे यकीन नहीं है कि यह वास्तव में मेरी वस्तुओं पर अतिरिक्त काम करने के लायक है बस ऐसा करने में सक्षम होना चाहिए।


मैं सामान्य रूप से स्मार्ट पॉइंटर्स का उपयोग उन ऑब्जेक्ट्स को लपेटने के लिए करता हूं जो किसी फ़ंक्शन / विधि से वापस किए जाते हैं।
जियोर्जियो

1
@ जियोर्जियो: यह निश्चित रूप से दोनों को धीमा और धीमा कर रहा है।
डेडएमजी

यदि आप एक साधारण ऑन-स्टैक ऑब्जेक्ट वापस करते हैं, तो आधुनिक कंपाइलरों को एक स्वचालित चाल प्रदर्शन करना चाहिए, इसलिए साझा ptrs आदि की कोई आवश्यकता नहीं है
क्रिश्चियन सेवेरिन
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.