वैश्विक चर के संदर्भ में लैंबडा फंक्शन म्यूटेबल कैप्चर का व्यवहार अंतर


22

मैंने पाया कि परिणाम संकलक में भिन्न होते हैं यदि मैं एक परिवर्तनीय कीवर्ड के साथ वैश्विक चर के संदर्भ को कैप्चर करने के लिए लैम्बडा का उपयोग करता हूं और फिर लैम्बडा फ़ंक्शन में मान को संशोधित करता हूं।

#include <stdio.h>
#include <functional>

int n = 100;

std::function<int()> f()
{
    int &m = n;
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

वीएस 2015 और जीसीसी से परिणाम (जी ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 201609):

100 223 100

क्लैंग ++ (क्लैंग संस्करण 3.8.0-2ubuntu4 (टैग / RELEASE_380 / अंतिम)) से परिणाम:

100 223 223

ऐसा क्यों होता है? क्या C ++ मानकों द्वारा इसकी अनुमति है?


क्लैंग का व्यवहार अभी भी ट्रंक पर मौजूद है।
अखरोट

ये सभी पुराने संकलक संस्करण हैं
एमएम

यह अभी भी क्लैंग के
विली

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

जवाबों:


16

एक लंबोदर खुद ही मूल्य ( std::reference_wrapperउस उद्देश्य के लिए उपयोग ) द्वारा एक संदर्भ को कैप्चर नहीं कर सकता है ।

आपके लैम्ब्डा में, वैल्यू द्वारा [m]कैप्चर mकिया जाता है (क्योंकि &कैप्चर में कोई भी नहीं है), इसलिए m(एक संदर्भ होने के नाते n) पहले डिरेल किया जाता है और उस चीज की एक प्रति जिसे वह संदर्भित करता है ( n) कैप्चर की जाती है। यह ऐसा करने से अलग नहीं है:

int &m = n;
int x = m; // <-- copy made!

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

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

आप संशोधित करना अपने लैम्ब्डा चाहते हैं n, पर कब्जा mबजाय संदर्भ द्वारा: [&m]। यह एक संदर्भ को दूसरे के लिए निर्दिष्ट करने से अलग नहीं है, जैसे:

int &m = n;
int &x = m; // <-- no copy made!

या, आप केवल mपूरी तरह से छुटकारा पा सकते हैं और nइसके बजाय संदर्भ द्वारा कब्जा कर सकते हैं [&n]:।

हालाँकि, चूँकि nवैश्विक दायरे में है, यह वास्तव में सभी पर कब्जा करने की आवश्यकता नहीं है, लैम्बडा इसे कैप्चर किए बिना विश्व स्तर पर एक्सेस कर सकता है:

return [] () -> int {
    n += 123;
    return n;
};

5

मुझे लगता है कि क्लैंग वास्तव में सही हो सकता है।

[Lambda.capture] / 11 के अनुसार , लैम्ब्डा में उपयोग की जाने वाली एक आईडी-अभिव्यक्ति का अर्थ है लैम्बडा की उप-कॉपी-कैप्चर की गई सदस्य केवल अगर यह ओड-उपयोग का गठन करती है । यदि ऐसा नहीं होता है, तो यह मूल इकाई को संदर्भित करता है । यह C ++ 11 के बाद से सभी C ++ संस्करणों पर लागू होता है।

C ++ 17 के अनुसार [basic.dev.odr] / 3 एक संदर्भ चर का उपयोग नहीं किया जाता है, अगर इसे करने के लिए lvalue-to-rvalue रूपांतरण लागू होता है, जो निरंतर अभिव्यक्ति देता है।

C ++ 20 ड्राफ्ट में हालांकि लैवल्यू-टू-रैवल्यू रूपांतरण की आवश्यकता समाप्त हो गई है और रूपांतरण को शामिल करने या न करने के लिए संबंधित मार्ग कई बार बदल गया। देखें राष्ट्रमंडल खेलों मुद्दा 1472 और राष्ट्रमंडल खेलों मुद्दा 1741 , साथ ही खुले राष्ट्रमंडल खेलों मुद्दा 2083

चूंकि mएक स्थिर अभिव्यक्ति (एक स्थिर भंडारण अवधि ऑब्जेक्ट का उल्लेख करते हुए) के साथ आरम्भ किया जाता है, इसका उपयोग करते हुए [expr.const] / 2.11.1 में प्रति अपवाद एक निरंतर अभिव्यक्ति प्राप्त करता है

हालांकि यह तब नहीं होता है जब लवलीन-टू-रिवल्यू रूपांतरण लागू होते हैं, क्योंकि nएक स्थिर अभिव्यक्ति में मूल्य उपयोगी नहीं है।

इसलिए, इस पर निर्भर करता है कि ओवल्यू-टू-रेवल्यू रूपांतरणों को ओड-यूज़ का निर्धारण करने में लागू किया जाना है या नहीं, जब आप लैम्बडा में उपयोग करते हैं, तो mयह लैम्ब्डा के सदस्य को संदर्भित कर सकता है या नहीं।

यदि रूपांतरण लागू किया जाना चाहिए, तो जीसीसी और एमएसवीसी सही हैं, अन्यथा क्लैंग है।

आप देख सकते हैं कि यदि आप mएक निरंतर अभिव्यक्ति नहीं होने के प्रारंभ को बदलते हैं तो क्लैंग इसे व्यवहार में परिवर्तन करता है :

#include <stdio.h>
#include <functional>

int n = 100;

void g() {}

std::function<int()> f()
{
    int &m = (g(), n);
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

इस मामले में सभी कंपाइलर सहमत हैं कि आउटपुट है

100 223 100

क्योंकि mलैम्ब्डा में क्लोजर के सदस्य को संदर्भित किया जाएगा, जो कि intटाइपिंग कॉपी-इन-रेफरेंस वेरिएबल mइन से है f


क्या वीएस / जीसीसी और क्लैंग दोनों परिणाम सही हैं? या उनमें से केवल एक?
विली

[basic.dev.odr] / 3 का कहना है कि वेरिएबल mको ओआरआर-एक्सप्रैस द्वारा प्रयोग किया जाता है, जब तक कि इसका नामकरण नहीं किया जाता है, जब तक कि लैवल-टू-रिवेल्यू रूपांतरण को लागू करना एक स्थिर अभिव्यक्ति नहीं होगी। [Expr.const] / (2.7) द्वारा, यह रूपांतरण एक मुख्य स्थिर अभिव्यक्ति नहीं होगा।
aschepler

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

1
m += 123;यहाँ mओड-यूज़ किया गया है।
ओलिव

1
मुझे लगता है कि क्लैंग मौजूदा शब्दांकन द्वारा सही है, और हालांकि मैंने इसमें कोई कमी नहीं की है, यहाँ प्रासंगिक परिवर्तन लगभग सभी डीआर हैं।
टीसी

4

यह C ++ 17 मानक द्वारा अनुमत नहीं है, लेकिन कुछ अन्य मानक ड्राफ्ट द्वारा यह हो सकता है। यह इस उत्तर में स्पष्ट नहीं किए गए कारणों से जटिल है।

[expr.prim.lambda.capture] / 10 :

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

इसका [m]मतलब है कि चर mको fकॉपी द्वारा कैप्चर किया गया है। इकाई mवस्तु का संदर्भ है, इसलिए बंद प्रकार का एक सदस्य है जिसका प्रकार संदर्भित प्रकार है। यही है, सदस्य का प्रकार है int, और नहीं int&

चूंकि mलैम्ब्डा बॉडी के अंदर का नाम क्लोजर ऑब्जेक्ट के मेंबर का नाम रखता है न कि इसमें f(और यह संदिग्ध भाग है), स्टेटमेंट m += 123;उस मेंबर को संशोधित करता है, जो एक अलग हैint ऑब्जेक्ट है ::n

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