Std :: function कैसे कार्यान्वित किया जाता है?


98

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

एक std::functionएक होना चाहिए तय आकार है, लेकिन यह एक ही तरह के किसी भी lambdas सहित callables के किसी भी प्रकार, रैप करने के लिए सक्षम होना चाहिए। इसे कैसे लागू किया जाता है? यदि std::functionआंतरिक रूप से अपने लक्ष्य के लिए एक पॉइंटर का उपयोग करता है, तो क्या होता है, जब std::functionउदाहरण की प्रतिलिपि बनाई जाती है या स्थानांतरित की जाती है? क्या कोई ढेर आवंटन शामिल हैं?


2
मैंने std::functionकुछ समय पहले gcc / stdlib कार्यान्वयन में देखा । यह मूल रूप से एक बहुरूपी वस्तु के लिए एक संभाल वर्ग है। आंतरिक बेस क्लास का एक व्युत्पन्न वर्ग मापदंडों को पकड़ने के लिए बनाया गया है, जो कि ढेर पर आवंटित किया गया है - फिर इसके लिए सूचक को एक सबोबिज के रूप में आयोजित किया जाता है std::function। मेरा मानना ​​है कि यह संदर्भ गिनती का उपयोग करता है जैसे std::shared_ptrनकल करना और हिलना।
एंड्रयू टॉमाज़ोस

4
ध्यान दें कि कार्यान्वयन जादू का उपयोग कर सकते हैं, अर्थात संकलक एक्सटेंशन पर निर्भर करते हैं जो आपके लिए अनुपलब्ध हैं। यह वास्तव में कुछ प्रकार के लक्षणों के लिए आवश्यक है। विशेष रूप से, trampolines मानक C ++ में उपलब्ध एक ज्ञात तकनीक है।
MSALERS 8

जवाबों:


78

कार्यान्वयन std::functionएक कार्यान्वयन से दूसरे में भिन्न हो सकता है, लेकिन मूल विचार यह है कि यह टाइप-इरेज़र का उपयोग करता है। हालांकि इसे करने के कई तरीके हैं, आप एक तुच्छ (इष्टतम नहीं) समाधान की कल्पना कर सकते हैं जो इस तरह से हो सकता है (सादगी के लिए विशिष्ट मामले के std::function<int (double)>लिए सरलीकृत)

struct callable_base {
   virtual int operator()(double d) = 0;
   virtual ~callable_base() {}
};
template <typename F>
struct callable : callable_base {
   F functor;
   callable(F functor) : functor(functor) {}
   virtual int operator()(double d) { return functor(d); }
};
class function_int_double {
   std::unique_ptr<callable_base> c;
public:
   template <typename F>
   function(F f) {
      c.reset(new callable<F>(f));
   }
   int operator()(double d) { return c(d); }
// ...
};

इस सरल दृष्टिकोण में, functionवस्तु बस unique_ptrआधार प्रकार के लिए संग्रहीत होगी । के साथ उपयोग किए जाने वाले प्रत्येक अलग-अलग फ़नकार के लिए function, आधार से प्राप्त एक नया प्रकार बनाया गया है और उस प्रकार की एक वस्तु को गतिशील रूप से त्वरित किया गया है। std::functionवस्तु एक ही आकार के हमेशा होता है और के रूप में ढेर में विभिन्न functors के लिए आवश्यक जगह आवंटित करेगा।

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


std::functionव्यवहार की प्रतियां कैसे होती हैं, इस मुद्दे के बारे में एक त्वरित परीक्षण इंगित करता है कि आंतरिक कॉल करने योग्य वस्तु की प्रतियां राज्य को साझा करने के बजाय की जाती हैं।

// g++4.8
int main() {
   int value = 5;
   typedef std::function<void()> fun;
   fun f1 = [=]() mutable { std::cout << value++ << '\n' };
   fun f2 = f1;
   f1();                    // prints 5
   fun f3 = f1;
   f2();                    // prints 5
   f3();                    // prints 6 (copy after first increment)
}

परीक्षण इंगित करता है कि f2कॉल करने योग्य इकाई की एक प्रति मिलती है, बजाय एक संदर्भ के। यदि कॉल करने योग्य इकाई को विभिन्न std::function<>वस्तुओं द्वारा साझा किया जाता है, तो कार्यक्रम का आउटपुट 5, 6, 7 होगा।


@Cole "कोल 9" जॉनसन ने अनुमान लगाया कि उन्होंने इसे खुद लिखा है
aaronman

8
@Cole "कोल 9" जॉनसन: यह वास्तविक कोड का एक निरीक्षण है, मैंने इसे केवल ब्राउज़र में टाइप किया है, इसलिए इसमें टाइपो और / या विभिन्न कारणों से संकलन करने में विफल हो सकता है। उत्तर में कोड सिर्फ यह पेश करने के लिए है कि किस प्रकार का क्षरण होता है / कार्यान्वित किया जा सकता है, यह स्पष्ट रूप से उत्पादन गुणवत्ता कोड नहीं है।
डेविड Rodríguez - dribeas

2
@MooDDuck: मेरा मानना ​​है कि लंबोदर मैथुन योग्य हैं (5.1.2 / 19), लेकिन यह सवाल नहीं है, बल्कि यह कि क्या std::functionआंतरिक वस्तु की नकल की गई थी, तो क्या शब्दार्थ सही होगा, और मुझे नहीं लगता कि ऐसा होना चाहिए (लगता है कि एक लैम्ब्डा जो एक मूल्य को पकड़ता है और एक दूसरे के अंदर संग्रहीत होता है std::function, अगर फ़ंक्शन स्टेट को कॉपी किया गया था, तो std::functionमानक एल्गोरिथ्म के अंदर की प्रतियों की संख्या अलग-अलग परिणामों में हो सकती है, जो अवांछित है।
डेविड रॉड्रिग्ज - ड्रिबेइज

1
@ MiklósHomolya: मैंने g ++ 4.8 के साथ परीक्षण किया और कार्यान्वयन आंतरिक स्थिति की प्रतिलिपि बनाता है। यदि कॉल करने योग्य इकाई को गतिशील आवंटन की आवश्यकता के लिए पर्याप्त बड़ा है, तो std::functionवसीयत की प्रतिलिपि आवंटन को ट्रिगर करेगी।
डेविड रॉड्रिग्ज़ - drieaseas

4
@ DavidRodríguez-dribeas साझा की गई स्थिति अवांछनीय होगी, क्योंकि छोटी वस्तु अनुकूलन का मतलब होगा कि आप साझा राज्य से एक कंपाइलर और कंपाइलर संस्करण निर्धारित आकार सीमा (असम्बद्ध वस्तु के रूप में छोटी वस्तु अनुकूलन साझा राज्य को अवरुद्ध) पर साझा राज्य से जाएंगे। यह समस्याग्रस्त लगता है।
यक्क - एडम नेवरामोंट

22

@David Rodríguez - dribeas का उत्तर टाइप-इरेज़र को दर्शाने के लिए अच्छा है, लेकिन इतना अच्छा नहीं है कि टाइप-इरेज़र में यह भी शामिल हो कि कैसे टाइप किए जाते हैं (उस उत्तर में फ़ंक्शन ऑब्जेक्ट कॉपी-कंस्ट्रक्टिव नहीं होगा)। उन व्यवहारों को functionऑब्जेक्ट में भी संग्रहीत किया जाता है, इसके अलावा फ़ंक्शनल डेटा भी।

उबंटू 14.04 gcc 4.8 से एसटीएल कार्यान्वयन में उपयोग की जाने वाली चाल, एक सामान्य फ़ंक्शन लिखने के लिए है, इसे प्रत्येक संभव फ़ंक्शनल प्रकार के साथ विशेषज्ञ करें, और उन्हें एक सार्वभौमिक फ़ंक्शन पॉइंटर प्रकार में डाल दें। इसलिए प्रकार की जानकारी मिट जाती है

मैं उस का एक सरलीकृत संस्करण तैयार है। आशा है कि यह मदद करेगा

#include <iostream>
#include <memory>

template <typename T>
class function;

template <typename R, typename... Args>
class function<R(Args...)>
{
    // function pointer types for the type-erasure behaviors
    // all these char* parameters are actually casted from some functor type
    typedef R (*invoke_fn_t)(char*, Args&&...);
    typedef void (*construct_fn_t)(char*, char*);
    typedef void (*destroy_fn_t)(char*);

    // type-aware generic functions for invoking
    // the specialization of these functions won't be capable with
    //   the above function pointer types, so we need some cast
    template <typename Functor>
    static R invoke_fn(Functor* fn, Args&&... args)
    {
        return (*fn)(std::forward<Args>(args)...);
    }

    template <typename Functor>
    static void construct_fn(Functor* construct_dst, Functor* construct_src)
    {
        // the functor type must be copy-constructible
        new (construct_dst) Functor(*construct_src);
    }

    template <typename Functor>
    static void destroy_fn(Functor* f)
    {
        f->~Functor();
    }

    // these pointers are storing behaviors
    invoke_fn_t invoke_f;
    construct_fn_t construct_f;
    destroy_fn_t destroy_f;

    // erase the type of any functor and store it into a char*
    // so the storage size should be obtained as well
    std::unique_ptr<char[]> data_ptr;
    size_t data_size;
public:
    function()
        : invoke_f(nullptr)
        , construct_f(nullptr)
        , destroy_f(nullptr)
        , data_ptr(nullptr)
        , data_size(0)
    {}

    // construct from any functor type
    template <typename Functor>
    function(Functor f)
        // specialize functions and erase their type info by casting
        : invoke_f(reinterpret_cast<invoke_fn_t>(invoke_fn<Functor>))
        , construct_f(reinterpret_cast<construct_fn_t>(construct_fn<Functor>))
        , destroy_f(reinterpret_cast<destroy_fn_t>(destroy_fn<Functor>))
        , data_ptr(new char[sizeof(Functor)])
        , data_size(sizeof(Functor))
    {
        // copy the functor to internal storage
        this->construct_f(this->data_ptr.get(), reinterpret_cast<char*>(&f));
    }

    // copy constructor
    function(function const& rhs)
        : invoke_f(rhs.invoke_f)
        , construct_f(rhs.construct_f)
        , destroy_f(rhs.destroy_f)
        , data_size(rhs.data_size)
    {
        if (this->invoke_f) {
            // when the source is not a null function, copy its internal functor
            this->data_ptr.reset(new char[this->data_size]);
            this->construct_f(this->data_ptr.get(), rhs.data_ptr.get());
        }
    }

    ~function()
    {
        if (data_ptr != nullptr) {
            this->destroy_f(this->data_ptr.get());
        }
    }

    // other constructors, from nullptr, from function pointers

    R operator()(Args&&... args)
    {
        return this->invoke_f(this->data_ptr.get(), std::forward<Args>(args)...);
    }
};

// examples
int main()
{
    int i = 0;
    auto fn = [i](std::string const& s) mutable
    {
        std::cout << ++i << ". " << s << std::endl;
    };
    fn("first");                                   // 1. first
    fn("second");                                  // 2. second

    // construct from lambda
    ::function<void(std::string const&)> f(fn);
    f("third");                                    // 3. third

    // copy from another function
    ::function<void(std::string const&)> g(f);
    f("forth - f");                                // 4. forth - f
    g("forth - g");                                // 4. forth - g

    // capture and copy non-trivial types like std::string
    std::string x("xxxx");
    ::function<void()> h([x]() { std::cout << x << std::endl; });
    h();

    ::function<void()> k(h);
    k();
    return 0;
}

एसटीएल संस्करण में कुछ अनुकूलन भी हैं

  • construct_fऔर destroy_fके रूप में कुछ बाइट्स को बचाने के लिए (एक अतिरिक्त पैरामीटर बताता है कि क्या करना है के साथ) एक समारोह सूचक में मिलाया जाता है
  • फंक्शन प्वाइंटर के साथ-साथ फंक्शन ऑब्जेक्ट को स्टोर करने के लिए कच्चे पॉइंटर्स का उपयोग किया जाता है union, ताकि जब functionकिसी फंक्शन पॉइंटर से ऑब्जेक्ट का निर्माण किया जाए, तो इसे सीधे स्टोर किया जा सकेगाunion हीप स्पेस बजाय

शायद एसटीएल कार्यान्वयन सबसे अच्छा समाधान नहीं है क्योंकि मैंने कुछ तेज़ कार्यान्वयन के बारे में सुना है । हालाँकि मेरा मानना ​​है कि अंतर्निहित तंत्र समान है।


20

कुछ प्रकार के तर्कों के लिए ("यदि f का लक्ष्य एक कॉल करने योग्य वस्तु है reference_wrapperया फ़ंक्शन पॉइंटर के माध्यम से पारित किया गया है ") std::function, तो कंस्ट्रक्टर किसी भी अपवाद को रोक देता है, इसलिए डायनेमिक मेमोरी का उपयोग करना प्रश्न से बाहर है। इस स्थिति के लिए, सभी डेटा को सीधे अंदर संग्रहीत किया जाना चाहिएstd::function ऑब्जेक्ट के ।

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

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


-6

एक std::functionभार के operator()यह एक functor वस्तु, लैम्ब्डा के काम उसी तरह बना रही है। यह मूल रूप से सदस्य चर के साथ एक संरचना बनाता है जिसे operator()फ़ंक्शन के अंदर पहुँचा जा सकता है । तो ध्यान में रखने के लिए मूल अवधारणा यह है कि एक लंबोदर एक वस्तु है (जिसे फंक्टर या फ़ंक्शन ऑब्जेक्ट कहा जाता है) एक फ़ंक्शन नहीं है। मानक का कहना है कि यदि इसे टाला जा सकता है तो गतिशील मेमोरी का उपयोग न करें।


1
संभवतः मनमाने ढंग से बड़े लैम्ब्डा एक निश्चित आकार में कैसे फिट हो सकते हैं std::function? यह यहाँ महत्वपूर्ण प्रश्न है।
मिकॉल्स होमोल्या

2
@ARonman: मैं गारंटी देता हूं कि प्रत्येक std::functionवस्तु एक ही आकार की है, और इसमें निहित लैम्ब्डा का आकार नहीं है।
मूविंग डक

5
@ARonman उसी तरह जिसमें प्रत्येक std::vector<T...> ऑब्जेक्ट में वास्तविक (कोपिलटाइम) निश्चित आकार होता है जो वास्तविक एलोकेटर उदाहरण / तत्वों की संख्या से स्वतंत्र होता है।
21

3
@ARonman: ठीक है, शायद आपको स्टैकओवरफ्लो प्रश्न ढूंढना चाहिए जो उत्तर देता है कि कैसे std :: फ़ंक्शन को इस तरह से लागू किया जाता है कि इसमें मनमाने ढंग से लैम्बडा हो सकते हैं: P
मूइंग डक

1
@ARonman: जब कॉल करने योग्य इकाई का निर्माण, निर्माण, असाइनमेंट में किया जाता है ... तो std::function<void ()> f;वहाँ आवंटित करने की कोई आवश्यकता नहीं है, std::function<void ()> f = [&]() { /* captures tons of variables */ };सबसे शायद आवंटित करता है। std::function<void()> f = &free_function;शायद या तो आवंटित नहीं करता है ...
डेविड रॉड्रिग्ज - drieaseas
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.