C ++ 11 के लैम्ब्डा को डिफ़ॉल्ट रूप से कैप्चर-बाय-वैल्यू के लिए "म्यूटेबल" कीवर्ड की आवश्यकता क्यों है?


256

संक्षिप्त उदाहरण:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

प्रश्न: हमें mutableकीवर्ड की आवश्यकता क्यों है ? यह पारंपरिक पैरामीटर से नामित कार्यों के लिए काफी अलग है। औचित्य क्या है?

मैं इस धारणा के तहत था कि कैप्चर-बाय-वैल्यू का पूरा बिंदु उपयोगकर्ता को अस्थायी बदलने की अनुमति देना है - अन्यथा मैं कैप्चर-बाय-संदर्भ का उपयोग करके लगभग हमेशा बेहतर हूं, क्या मैं नहीं हूं?

कोई आत्मज्ञान?

(मैं MSVC2010 का उपयोग कर रहा हूँ वैसे। AFAIK यह मानक होना चाहिए)


101
अच्छा प्रश्न; हालांकि मुझे खुशी है कि कुछ constडिफ़ॉल्ट रूप से है!
xtofl

3
उत्तर नहीं है, लेकिन मुझे लगता है कि यह एक समझदारी वाली बात है: यदि आप किसी चीज को महत्व देते हैं, तो आपको इसे केवल एक स्थानीय चर में 1 कॉपी को बचाने के लिए नहीं बदलना चाहिए। कम से कम आप n बदलने की गलती नहीं करेंगे।
स्टेफानव

8
@xtofl: सुनिश्चित नहीं है कि यह अच्छा है, जब बाकी सब कुछ constडिफ़ॉल्ट रूप से नहीं है।
kizzx2

8
@ तमसे सजेलेई: तर्क शुरू करने के लिए नहीं, लेकिन आईएमएचओ की अवधारणा "सीखने में आसान" सी ++ भाषा में कोई स्थान नहीं है, खासकर आधुनिक दिनों में। वैसे भी: P
kizzx2

3
"कैप्चर-बाय-वैल्यू का पूरा बिंदु उपयोगकर्ता को अस्थायी बदलने की अनुमति देना है" - नहीं, पूरे बिंदु यह है कि लैम्ब्डा किसी भी कैप्चर किए गए चर के जीवनकाल से परे वैध हो सकता है। यदि C ++ लैंबडास में केवल कैप्चर-अप-रेफरी होता है, तो वे कई परिदृश्यों में अनुपयोगी होंगे।
सेबेस्टियन रेडल

जवाबों:


230

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


7
यह लाभप्रद है। मैं पूरी तरह सहमत हूँ। C ++ 0x में, हालांकि, मैं यह नहीं देखता कि डिफ़ॉल्ट कैसे ऊपर लागू करने में मदद करता है। विचार करें कि मैं लंबोदर के प्राप्त अंत पर हूं, जैसे मैं हूं void f(const std::function<int(int)> g)। मुझे इस बात की गारंटी कैसे दी जाती है कि gवास्तव में यह पारदर्शी है ? gआपूर्तिकर्ता mutableवैसे भी इस्तेमाल किया जा सकता है । इसलिए मुझे पता नहीं चलेगा। दूसरी ओर, यदि डिफ़ॉल्ट गैर है const, और लोगों को जोड़ना होगा constके बजाय mutableसमारोह वस्तुओं के लिए, संकलक वास्तव में लागू कर सकते हैं const std::function<int(int)>हिस्सा है और अब fमान सकते हैं कि gहै const, नहीं?
kizzx2

8
@ kizzx2: C ++ में, कुछ भी लागू नहीं किया गया है , केवल सुझाव दिया गया है। हमेशा की तरह, यदि आप कुछ बेवकूफी करते हैं (संदर्भित पारदर्शिता के लिए आवश्यक दस्तावेज और फिर गैर-संदर्भात्मक-पारदर्शी फ़ंक्शन पास करते हैं), तो आपको जो कुछ भी आता है वह आपको मिल जाता है।
पिल्ला

6
इस जवाब ने मेरी आँखें खोल दीं। पहले, मैंने सोचा था कि इस मामले में लैम्ब्डा केवल वर्तमान "रन" के लिए एक प्रति म्यूट करता है।
Zsolt Szatmari

4
@ZsoltSzatmari आपकी टिप्पणी ने मेरी आँखें खोल दीं! : -DI को इस उत्तर का सही अर्थ नहीं मिला जब तक कि मैं आपकी टिप्पणी नहीं पढ़ता।
जिनदास

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

103

आपका कोड इसके बराबर है:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

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

आप उस वर्ग के सदस्यों के रूप में [] (स्पष्ट या निहित रूप से) के अंदर कैद किए गए सभी चर के बारे में भी सोच सकते हैं: [=] के लिए वस्तुओं की प्रतियां या [और] के लिए वस्तुओं के संदर्भ में। जब आप अपने लैम्ब्डा की घोषणा करते हैं तो उन्हें इनिशियलाइज़ किया जाता है जैसे कि कोई छिपा हुआ कंस्ट्रक्टर था।


5
जबकि का एक अच्छा विवरण क्या एक constया mutableलैम्ब्डा अगर बराबर उपयोगकर्ता परिभाषित प्रकार के रूप में लागू की तरह लग रहे हैं, प्रश्न (शीर्षक में के रूप में और टिप्पणियों में ओपी की व्याख्या के अनुसार) है क्यों const तो यह है कि यह उत्तर नहीं मिलता है डिफ़ॉल्ट है।
अंडरस्कोर_ड

36

मैं इस धारणा के तहत था कि कैप्चर-बाय-वैल्यू का पूरा बिंदु उपयोगकर्ता को अस्थायी बदलने की अनुमति देना है - अन्यथा मैं कैप्चर-बाय-संदर्भ का उपयोग करके लगभग हमेशा बेहतर हूं, क्या मैं नहीं हूं?

सवाल यह है कि क्या यह "लगभग" है? लैम्बदास को लौटाने या पास करने के लिए एक लगातार उपयोग-मामला प्रतीत होता है:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

मुझे लगता है कि mutable"लगभग" का मामला नहीं है। मैं "कैप्चर-बाय-वैल्यू" पर विचार करता हूं जैसे "कैप्चर की गई इकाई के मरने के बाद मुझे इसके मूल्य का उपयोग करने की अनुमति देता है" के बजाय "मुझे इसकी एक प्रति बदलने की अनुमति दें"। लेकिन शायद यह तर्क दिया जा सकता है।


2
अच्छा उदाहरण। कैप्चर-बाय-वैल्यू के उपयोग के लिए यह बहुत मजबूत उपयोग-मामला है। लेकिन यह डिफ़ॉल्ट क्यों होता है const? यह किस उद्देश्य को प्राप्त करता है? mutableयहाँ जगह से बाहर लगता है, जब constहै नहीं में "लगभग" डिफ़ॉल्ट (: पी) भाषा का सब कुछ।
kizzx2

8
@ kizzx2: मेरी इच्छा constथी कि डिफ़ॉल्ट, कम से कम लोग कांस्टीट्यूशन पर विचार करने के लिए मजबूर होंगे: /
मैथ्यू एम।

1
@ kizzx2 लैम्ब्डा पेपर में देख रहा है, यह मुझे प्रतीत होता है कि वे इसे डिफ़ॉल्ट बनाते हैं constताकि वे इसे कॉल कर सकें कि लैम्ब्डा ऑब्जेक्ट कांस्टेबल है या नहीं। उदाहरण के लिए वे इसे फंक्शन में पास कर सकते थे std::function<void()> const&। लैम्ब्डा को इसकी कैप्चर की गई प्रतियों को बदलने की अनुमति देने के लिए, शुरुआती कागजात में क्लोजर के डेटा सदस्यों को mutableआंतरिक रूप से स्वचालित रूप से परिभाषित किया गया था। अब आपको मैन्युअल रूप mutableसे लैम्ब्डा एक्सप्रेशन में रखना होगा । हालांकि मुझे एक विस्तृत तर्क नहीं मिला है।
जोहान्स स्काउब -

2
कुछ विवरणों के लिए open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2651.pdf देखें ।
जोहान्स शाउब -

5
इस बिंदु पर, मेरे लिए, "वास्तविक" उत्तर / तर्क प्रतीत होता है "वे एक कार्यान्वयन विवरण के आसपास काम करने में विफल रहे": /
kizzx2

32

FWIW, हर्ब सटर, C ++ मानकीकरण समिति के एक प्रसिद्ध सदस्य, लैम्बडा सुधार और प्रयोज्यता मुद्दों में उस प्रश्न का एक अलग उत्तर प्रदान करता है :

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

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

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

उनका पेपर इस बारे में है कि इसे C ++ 14 में क्यों बदला जाना चाहिए। यह विशेष रूप से इस विशेषता के संबंध में "कमेटी के सदस्यों के दिमाग में क्या है" जानना चाहते हैं, तो यह छोटा, अच्छी तरह से लिखा गया है।


16

आपको यह सोचने की ज़रूरत है कि आपके लैम्ब्डा फ़ंक्शन का क्लोजर प्रकार क्या है । हर बार जब आप एक लैम्ब्डा अभिव्यक्ति की घोषणा करते हैं, तो कंपाइलर एक क्लोजर प्रकार बनाता है, जो कि विशेषताओं ( पर्यावरण जहां लैम्बडा अभिव्यक्ति घोषित किया गया है) और फ़ंक्शन कॉल के साथ एक अनाम वर्ग घोषणा से कम नहीं है ::operator()। जब आप कॉपी-बाय-वैल्यू का उपयोग करके किसी वैरिएबल को कैप्चर करते हैं , तो कंपाइलर constक्लोजर प्रकार में एक नई विशेषता पैदा करेगा , इसलिए आप इसे लैम्ब्डा एक्सप्रेशन के अंदर नहीं बदल सकते क्योंकि यह "रीड-ओनली" विशेषता है, यही कारण है कि वे इसे " क्लोजर " कहें , क्योंकि किसी तरह से, आप अपने लैम्बडा एक्सप्रेशन को ऊपरी दायरे से लेम्बडा के दायरे में कॉपी करके बंद कर रहे हैं।mutableपर कब्जा कर लिया इकाई non-constअपने बंद प्रकार का एक विशेषता बन जाएगा । यह वह कारण है जो मूल्य द्वारा कैप्चर किए गए परिवर्तनशील चर में किए गए परिवर्तनों को ऊपरी दायरे में प्रचारित नहीं किया जाता है, लेकिन स्टेटफुल लैंबडा के अंदर रहते हैं। हमेशा अपने लैम्ब्डा अभिव्यक्ति के परिणामस्वरूप बंद होने की कल्पना करने की कोशिश करें, जिससे मुझे बहुत मदद मिली, और मुझे उम्मीद है कि यह आपकी भी मदद कर सकता है।


14

इस मसौदे को देखें , 5.1.2 के तहत [expr.prim.lambda], उपखंड 5:

लैम्ब्डा-एक्सप्रेशन के लिए क्लोजर प्रकार में एक सार्वजनिक इनलाइन फ़ंक्शन कॉल ऑपरेटर (13.5.4) होता है, जिसके पैरामीटर और रिटर्न प्रकार क्रमशः लैम्ब्डा-एक्सप्रेशन के पैरामीटर-घोषणा-खंड और अनुगामी-प्रकार द्वारा वर्णित किए जाते हैं। इस फ़ंक्शन कॉल ऑपरेटर को कांस्टेबल (9.3.1) घोषित किया जाता है, यदि और केवल यदि लैम्बडाक्प्रेशन के पैरामीटर-डिक्लेरेशन-क्लॉज को म्यूट करने के बाद नहीं किया जाता है।

Litb की टिप्पणी पर संपादित करें: हो सकता है कि उन्होंने कैप्चर-बाय-वैल्यू के बारे में सोचा हो, ताकि चर के बाहर परिवर्तन लंबोदर के अंदर प्रतिबिंबित न हों? संदर्भ दोनों तरह से काम करते हैं, इसलिए यह मेरी व्याख्या है। हालांकि यह किसी भी अच्छा है पता नहीं है।

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


यह मानक है, लेकिन उन्होंने इसे इस तरह क्यों लिखा?
kizzx2

@ kizzx2: मेरा स्पष्टीकरण सीधे उस उद्धरण के तहत है। :) यह इस बात से थोड़ा संबंधित है कि कब्जा की गई वस्तुओं के जीवनकाल के बारे में क्या कहता है, लेकिन यह भी थोड़ा आगे बढ़ता है।
Xio

@ Xeo: ओह हाँ, मैंने याद किया कि: P यह कैप्चर-वैल्यू के अच्छे उपयोग के लिए एक और अच्छा स्पष्टीकरण है । लेकिन यह constडिफ़ॉल्ट रूप से क्यों होना चाहिए ? मुझे पहले से ही एक नई प्रति मिल गई है, यह अजीब लगता है कि मुझे इसे बदलने न दें - विशेष रूप से यह मुख्य रूप से कुछ गलत नहीं है - वे बस मुझे जोड़ना चाहते हैं mutable
kizzx2

मेरा मानना ​​है कि एक नया जेनरल फंक्शन डिक्लेरेशन सिंटेक्स बनाने की कोशिश की गई, जिसका नाम लाम्बाडा जैसा है। यह डिफ़ॉल्ट रूप से सबकुछ बनाकर अन्य समस्याओं को ठीक करने वाला था। कभी पूरा नहीं हुआ, लेकिन विचारों को लंबोदर परिभाषा पर रगड़ दिया गया।
बो पर्सन

2
@ kizzx2 - यदि हम फिर से शुरू कर सकते हैं, तो शायद हमारे पास varएक खोजशब्द के रूप में परिवर्तन और निरंतरता बाकी सब चीजों के लिए डिफ़ॉल्ट होने की अनुमति होगी। अब हम नहीं करते, इसलिए हमें उसी के साथ रहना होगा। IMO, C ++ 2011 सब कुछ देखते हुए बहुत अच्छी तरह से सामने आया।
बो पर्सन

11

मैं इस धारणा के तहत था कि कैप्चर-बाय-वैल्यू का पूरा बिंदु उपयोगकर्ता को अस्थायी बदलने की अनुमति देना है - अन्यथा मैं कैप्चर-बाय-संदर्भ का उपयोग करके लगभग हमेशा बेहतर हूं, क्या मैं नहीं हूं?

nहै एक अस्थायी। n लैम्बडा-फंक्शन-ऑब्जेक्ट का एक सदस्य है जिसे आप लैम्बडा एक्सप्रेशन के साथ बनाते हैं। डिफ़ॉल्ट अपेक्षा यह है कि आपके लैम्ब्डा को कॉल करने से इसकी स्थिति को संशोधित नहीं किया जाता है, इसलिए यह आपको गलती से संशोधित करने से रोकने के लिए बाध्य है n


1
संपूर्ण लंबोदर वस्तु एक अस्थायी है, इसके सदस्यों का अस्थायी जीवनकाल भी है।
बेन वोइगट

2
@ बीन: IIRC, मैं इस मुद्दे का जिक्र कर रहा था कि जब कोई "अस्थायी" कहता है, तो मैं इसका मतलब यह समझता हूं कि अनाम अस्थायी वस्तु है, जो कि लंबोदर ही है, लेकिन यह सदस्य नहीं हैं। और यह भी कि लंबोदर के "अंदर" से, यह वास्तव में कोई फर्क नहीं पड़ता कि लंबोदर स्वयं अस्थायी है या नहीं। प्रश्न को फिर से पढ़ना हालांकि यह प्रतीत होता है कि ओपी को "लंबोदर के अंदर" कहने का मतलब है जब उसने "अस्थायी" कहा।
मार्टिन बा

6

आपको समझना होगा कि कैप्चर का मतलब क्या है! यह तर्क नहीं गुजर रहा है! आइए कुछ कोड नमूने देखें:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

जैसा कि आप देख सकते हैं कि भले ही लैम्ब्डा xको बदल दिया गया हो 20लेकिन अभी भी 10 लौट रहा है ( xअभी भी 5लैम्ब्डा के अंदर है) लैम्बडा के xअंदर बदलने का मतलब है कि लैम्बडा को प्रत्येक कॉल पर स्वयं बदलना (लैम्बडा प्रत्येक कॉल पर म्यूट करना है)। शुद्धता को लागू करने के लिए मानक ने mutableखोजशब्द प्रस्तुत किया । एक लैम्ब्डा को आप के रूप में निर्दिष्ट करके आप कह रहे हैं कि लैम्बडा के प्रत्येक कॉल से लैम्बडा में परिवर्तन हो सकता है। एक और उदाहरण देखते हैं:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

उपरोक्त उदाहरण से पता चलता है कि लैम्ब्डा को परिवर्तनशील बनाकर, लैम्बडा के xअंदर बदलते हुए लैम्बडा को प्रत्येक कॉल पर एक नए मूल्य के साथ लैम्ब्डा में बदल xदिया जाता है x, जिसका मुख्य कार्य के वास्तविक मूल्य से कोई लेना-देना नहीं है।


4

अब लंबोदरmutable घोषणाओं की आवश्यकता को कम करने का प्रस्ताव है : n3424


इस बारे में कोई जानकारी क्या है? मुझे व्यक्तिगत रूप से लगता है कि यह एक बुरा विचार है, क्योंकि नए "मनमाने ढंग से अभिव्यक्ति पर कब्जा" से दर्द के अधिकांश बिंदु सुचारू हो जाते हैं।
बेन वोइगट

1
@BenVoigt हाँ यह बदलाव के लिए एक बदलाव की तरह लगता है।
माइल राउत

3
@BenVoigt हालांकि निष्पक्ष होने के लिए, मुझे उम्मीद है कि शायद कई C ++ डेवलपर्स हैं जो नहीं जानते कि mutableयह C ++ में भी एक कीवर्ड है।
माइल राउत

1

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

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

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

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