C ++ 11 लैंबडा कार्यान्वयन और मेमोरी मॉडल


92

मुझे C ++ 11 क्लोजर के बारे में सही तरीके से सोचने std::functionऔर कैसे लागू किया जाता है और मेमोरी को कैसे संभाला जाता है , इसके बारे में कुछ जानकारी चाहिए ।

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

इसलिए मैं C ++ लैम्ब्डा का उपयोग करने या न करने की बेहतर समझ विकसित करना चाहता हूं।

मेरी वर्तमान समझ यह है कि बिना किसी बंद किए हुए लैंबडा बिलकुल सी कॉलबैक जैसा है। हालांकि, जब पर्यावरण को मूल्य या संदर्भ द्वारा कैप्चर किया जाता है, तो स्टैक पर एक अनाम वस्तु बनाई जाती है। जब किसी फ़ंक्शन से वैल्यू-क्लोजर लौटाया जाना चाहिए, तो कोई इसे लपेटता है std::function। इस मामले में क्लोजर मेमोरी का क्या होता है? क्या यह ढेर से ढेर की नकल है? क्या जब भी इसे मुक्त किया std::functionजाता है, तब इसे मुक्त कर दिया जाता है, क्या इसे संदर्भ-रूप में गिना जाता है std::shared_ptr?

मुझे लगता है कि एक वास्तविक समय प्रणाली में मैं लंबोदा कार्यों की एक श्रृंखला स्थापित कर सकता हूं, बी को ए को एक निरंतरता तर्क के रूप में पारित कर सकता हूं, ताकि एक प्रसंस्करण पाइपलाइन A->Bबनाई जाए। इस मामले में, ए और बी क्लोजर एक बार आवंटित किया जाएगा। हालांकि मुझे यकीन नहीं है कि ये स्टैक या ढेर पर आवंटित किए जाएंगे। हालांकि सामान्य तौर पर यह वास्तविक समय प्रणाली में उपयोग करने के लिए सुरक्षित लगता है। दूसरी ओर यदि B कुछ लंबो फंक्शन C का निर्माण करता है, जिसे वह लौटाता है, तो C के लिए मेमोरी को बार-बार आवंटित और डील किया जाएगा, जो वास्तविक समय के उपयोग के लिए स्वीकार्य नहीं होगा।

छद्म कोड में, एक डीएसपी लूप, जो मुझे लगता है कि वास्तविक समय में सुरक्षित होने वाला है। मैं प्रसंस्करण ब्लॉक ए और फिर बी करना चाहता हूं, जहां ए अपनी दलील कहता है। ये दोनों कार्य std::functionवस्तुओं को वापस करते हैं, इसलिए fएक std::functionवस्तु होगी , जहां इसका वातावरण ढेर पर संग्रहीत है:

auto f = A(B);  // A returns a function which calls B
                // Memory for the function returned by A is on the heap?
                // Note that A and B may maintain a state
                // via mutable value-closure!
for (t=0; t<1000; t++) {
    y = f(t)
}

और जो मुझे लगता है कि वास्तविक समय कोड में उपयोग करने के लिए बुरा हो सकता है:

for (t=0; t<1000; t++) {
    y = A(B)(t);
}

और जहाँ मुझे लगता है कि स्टैक मेमोरी का उपयोग बंद होने की संभावना है:

freq = 220;
A = 2;
for (t=0; t<1000; t++) {
    y = [=](int t){ return sin(t*freq)*A; }
}

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

क्या ये सही है? धन्यवाद।


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

यह इस बारे में एक प्रश्न प्रतीत होता है कि क्या std::functionअपने राज्य को ढेर पर संग्रहीत करता है या नहीं, और इसका लंबोदा से कोई लेना-देना नहीं है। क्या वह सही है?
मूइंग डक

8
किसी भी गलतफहमी के मामले में बस इसे उगलने के लिए: एक लम्बा अभिव्यक्ति नहीं है std::function!!
Xio

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

2
@ सी + 14 के बाद से आप एक autoप्रकार से एक लैंबडा को रिटर्न प्रकार के साथ वापस कर सकते हैं ।
ओकाल्टिस्ट

जवाबों:


100

मेरी वर्तमान समझ यह है कि बिना किसी बंद किए हुए लैंबडा बिलकुल सी कॉलबैक जैसा है। हालांकि, जब पर्यावरण को मूल्य या संदर्भ द्वारा कैप्चर किया जाता है, तो स्टैक पर एक अनाम वस्तु बनाई जाती है।

नहीं; यह हमेशा एक अज्ञात प्रकार के साथ C ++ ऑब्जेक्ट होता है, जिसे स्टैक पर बनाया जाता है। एक पर कब्जा कम लैम्ब्डा किया जा सकता है परिवर्तित एक समारोह सूचक में (हालांकि चाहे वह सी सम्मेलनों फोन करने के लिए उपयुक्त है कार्यान्वयन निर्भर है), लेकिन इसका मतलब यह नहीं है कि यह है एक समारोह सूचक।

जब किसी मान को किसी फ़ंक्शन से वापस किया जाना चाहिए, तो कोई इसे std :: function में लपेटता है। इस मामले में क्लोजर मेमोरी का क्या होता है?

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

auto lamb = []() {return 5;};

lambएक स्टैक ऑब्जेक्ट है। इसका एक निर्माता और विध्वंसक है। और यह उसके लिए सभी C ++ नियमों का पालन करेगा। उस प्रकार के lambमानों / मानों को शामिल किया जाएगा जो कैप्चर किए गए हैं; वे उस वस्तु के सदस्य होंगे, जैसे किसी अन्य प्रकार के किसी अन्य वस्तु के सदस्य।

आप इसे दे सकते हैं std::function:

auto func_lamb = std::function<int()>(lamb);

इस मामले में, इसे के मूल्य की एक प्रति मिल जाएगी lamb। यदि lambमूल्य से कुछ भी पकड़ा गया था, तो उन मूल्यों की दो प्रतियां होंगी; एक में lamb, और एक में func_lamb

जब स्टैक चर की सफाई के नियमों के अनुसार, वर्तमान गुंजाइश समाप्त हो जाती है, func_lambतब नष्ट हो जाएगी lamb

आप आसानी से ढेर पर एक आवंटित कर सकते हैं:

auto func_lamb_ptr = new std::function<int()>(lamb);

वास्तव में, जहाँ किसी सामग्री की मेमोरी std::functionकार्यान्वयन-निर्भर होती है, लेकिन std::functionआमतौर पर नियोजित प्रकार-इरेज़र को कम से कम मेमोरी आवंटन की आवश्यकता होती है। यही कारण है कि std::functionकंस्ट्रक्टर एक एलोकेटर ले सकता है।

क्या इसे मुक्त किया जाता है जब भी std :: फ़ंक्शन को मुक्त किया जाता है, अर्थात, इसे संदर्भ std :: shared_ptr की तरह गिना जाता है?

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

इस प्रकार संदर्भ गणना की कोई आवश्यकता नहीं है।

आपके द्वारा दिया गया बाकी सब कुछ सही है, यह मानते हुए कि "मेमोरी आवंटन" "वास्तविक समय कोड में उपयोग करने के लिए खराब" के बराबर है।


1
बहुत बढ़िया स्पष्टीकरण, धन्यवाद। तो सृजन std::functionवह बिंदु है जिस पर स्मृति को आवंटित और कॉपी किया जाता है। ऐसा लगता है कि एक बंद करने के लिए कोई रास्ता नहीं है (क्योंकि उन्हें स्टैक पर आवंटित किया गया है), पहली बार में नकल के बिना std::function, हाँ?
स्टीव

3
@ पाठ: हाँ; गुंजाइश से बाहर निकलने के लिए आपको किसी प्रकार के कंटेनर में एक लैम्ब्डा लपेटना होगा।
निकोल बोल

क्या पूरे फ़ंक्शन का कोड कॉपी किया गया है, या मूल फ़ंक्शन संकलन-समय-आबंटित है और क्लोज़-ओवर मानों को पारित किया है?
लेलमेड्डन

मैं यह जोड़ना चाहता हूं कि मानक कम या ज्यादा अप्रत्यक्ष रूप से जनादेश (.11 20.8.11.2.1 [func.wrap.func.con]) 5) है कि अगर एक लैम्ब्डा कुछ भी कब्जा नहीं करता है, तो इसे std::functionगतिशील मेमोरी के साथ एक ऑब्जेक्ट में संग्रहीत किया जा सकता है। आवंटन चल रहा है।
5gon12eder

2
@ यक: आप "बड़े" को कैसे परिभाषित करते हैं? क्या राज्य के दो बिंदुओं के साथ एक वस्तु "बड़ी" है? कैसे के बारे में 3 या 4? इसके अलावा, ऑब्जेक्ट का आकार एकमात्र मुद्दा नहीं है; यदि ऑब्जेक्ट nothrow-moveable नहीं है, तो इसे एक आवंटन में संग्रहीत किया जाना चाहिए, क्योंकि functionएक noexcept मूव कंस्ट्रक्टर है। "आम तौर पर आवश्यकताएं" कहने का पूरा मतलब यह है कि मैं " हमेशा की आवश्यकता है" नहीं कह रहा हूं : ऐसी परिस्थितियां हैं जहां कोई आवंटन नहीं किया जाएगा।
निकोल बोल

0

C ++ लैम्ब्डा केवल ओवरलोड के साथ एक अनाम शर्करा के आसपास (अनाम) फ़नकार वर्ग है operator()और std::functionकॉलबल्स (यानी फंक्शनलर्स, लैम्ब्डा, सी-फ़ंक्शंस, ...) के चारों ओर एक आवरण है जो वर्तमान से "सॉलिड हेंडा ऑब्जेक्ट" को कॉपी करता है। ढेर गुंजाइश - ढेर करने के लिए ।

वास्तविक कंस्ट्रक्टर / रिलोकैट्स की संख्या का परीक्षण करने के लिए मैंने एक परीक्षण किया (शेपिंग के दूसरे स्तर का उपयोग करके लेकिन इसके मामले में नहीं)। अपने आप को देखो:

#include <memory>
#include <string>
#include <iostream>

class Functor {
    std::string greeting;
public:

    Functor(const Functor &rhs) {
        this->greeting = rhs.greeting;
        std::cout << "Copy-Ctor \n";
    }
    Functor(std::string _greeting="Hello!"): greeting { _greeting } {
        std::cout << "Ctor \n";
    }

    Functor & operator=(const Functor & rhs) {
        greeting = rhs.greeting;
        std::cout << "Copy-assigned\n";
        return *this;
    }

    virtual ~Functor() {
        std::cout << "Dtor\n";
    }

    void operator()()
    {
        std::cout << "hey" << "\n";
    }
};

auto getFpp() {
    std::shared_ptr<std::function<void()>> fp = std::make_shared<std::function<void()>>(Functor{}
    );
    (*fp)();
    return fp;
}

int main() {
    auto f = getFpp();
    (*f)();
}

यह यह उत्पादन करता है:

Ctor 
Copy-Ctor 
Copy-Ctor 
Dtor
Dtor
hey
hey
Dtor

स्टैक-आवंटित लैम्ब्डा ऑब्जेक्ट के लिए सटीक रूप से सेटर्स / डक्टर्स का एक ही सेट बुलाया जाएगा! (अब यह स्टैक आवंटन के लिए Ctor को कॉल करता है, प्रतिलिपि-ctor (+ हीप आवंटन) को std :: function में निर्माण करने के लिए और साझा करने के लिए एक और one_ptr हीप आवंटन + फ़ंक्शन का निर्माण)

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