हाल ही में मैं प्रतिद्वंद्विता और सही अग्रेषण के बारे में जानने की कोशिश कर रहा हूं। कुछ संरचनाओं के साथ खेलते समय मुझे कंपाइलर्स और ऑप्टिमाइज़ेशन स्तरों को स्विच करते समय कुछ अजीब व्यवहार आया।

बिना ऑप्टिमाइज़ेशन चालू किए GCC पर समान कोड को संकलित करने से अपेक्षित परिणाम प्राप्त होंगे, हालांकि किसी भी अनुकूलन स्तर को चालू करने से मेरा सारा कोड हटा दिया जाएगा। बिना किसी अनुकूलन के क्लैंग पर समान कोड संकलित करने से भी अपेक्षित परिणाम प्राप्त होते हैं। फिर क्लैंग पर ऑप्टिमाइज़ेशन चालू करने से अभी भी अपेक्षित परिणाम प्राप्त होंगे।

मुझे पता है कि यह अपरिभाषित व्यवहार चिल्लाता है लेकिन मैं यह नहीं समझ सकता कि वास्तव में क्या गलत हो रहा है और दो कंपाइलर्स के बीच विसंगति का कारण क्या है।

gcc -O0 -std=c++17 -Wall -Wextra

https://godbolt.org/z/5xY1Gz

gcc -O3 -std=c++17 -Wall -Wextra

https://godbolt.org/z/fE3TE5

clang -O0 -std=c++17 -Wall -Wextra

https://godbolt.org/z/W98fh8

clang -O3 -std=c++17 -Wall -Wextra

https://godbolt.org/z/6sEo8j

#include <utility>

// lambda_t is the type of thing we want to call.
// capture_t is the type of a helper object that 
// contains all all parameters meant to be passed to the callable
template< class lambda_t, class capture_t >
struct CallObject {

    lambda_t  m_lambda;
    capture_t m_args;

    typedef decltype( m_args(m_lambda) ) return_t;

    //Construct the CallObject by perfect forwarding which is
    //neccessary as they may these are lambda which will have
    //captured objects and we dont want uneccessary copies
    //while passing these around
    CallObject( lambda_t&& p_lambda, capture_t&& p_args ) :
        m_lambda{ std::forward<lambda_t>(p_lambda) },
        m_args  { std::forward<capture_t>(p_args) }
    {

    }

    //Applies the arguments captured in m_args to the thing
    //we actually want to call
    return_t invoke() {
        return m_args(m_lambda);
    }

    //Deleting special members for testing purposes
    CallObject() = delete;
    CallObject( const CallObject& ) = delete;
    CallObject( CallObject&& ) = delete;
    CallObject& operator=( const CallObject& ) = delete;
    CallObject& operator=( CallObject&& ) = delete;
};

//Factory helper function that is needed to create a helper
//object that contains all the paremeters required for the 
//callable. Aswell as for helping to properly templatize
//the CallObject
template< class lambda_t, class ... Tn >
auto Factory( lambda_t&& p_lambda, Tn&& ... p_argn ){

    //Using a lambda as helper object to contain all the required paramters for the callable
    //This conviently allows for storing value, references and so on
    auto x = [&p_argn...]( lambda_t& pp_lambda ) mutable -> decltype(auto) {

        return pp_lambda( std::forward<decltype(p_argn)>(p_argn) ... );
    };

    typedef decltype(x) xt;
    //explicit templetization is not needed in this case but
    //for the sake of readability it needed here since we then
    //need to forward the lambda that captures the arguments
    return CallObject< lambda_t, xt >( std::forward<lambda_t>(p_lambda), std::forward<xt>(x) );
}

int main(){

    auto xx = Factory( []( int a, int b ){

        return a+b;

    }, 10, 3 );

    int q = xx.invoke();

    return q;
}
c++
4
mradek92 11 अगस्त 2020, 08:08

3 जवाब

सबसे बढ़िया उत्तर

GCC O3 पर मेरा कोड क्यों हटाता है

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

बस यह पता नहीं लगा सकता कि वास्तव में क्या गलत हो रहा है और दो संकलक के बीच विसंगति का कारण क्या है।

कार्यक्रम का व्यवहार अपरिभाषित है। संकलक, या किसी विशेष व्यवहार के बीच विसंगति न होने की अपेक्षा करने का कोई कारण नहीं है।

कार्यक्रम का व्यवहार अपरिभाषित है।

लेकिन क्यों?

यहां:

auto xx = Factory(the_lambda, 10, 3);

आप फ़ंक्शन में अक्षर पास करते हैं, जो प्रचलित हैं।

auto Factory( lambda_t&& p_lambda, Tn&& ... p_argn )

फ़ंक्शन उन्हें संदर्भ द्वारा स्वीकार करता है। इसलिए अस्थायी वस्तुएं बनाई जाती हैं, जिनका जीवनकाल पूर्ण अभिव्यक्ति के अंत तक विस्तारित होता है (जो कि तर्क संदर्भों के जीवनकाल से अधिक लंबा है, इसलिए अस्थायी लोगों का जीवनकाल विस्तारित नहीं होता है)।

auto x = [&p_argn...]( //...

संदर्भित अस्थायी को लैम्ब्डा में संग्रहीत किया जाता है ... संदर्भ द्वारा। लैम्ब्डा में किसी भी समय एक पूर्णांक संग्रहीत नहीं होता है।

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


इस तरह की गलतियाँ यही कारण हैं कि std::thread, std::bind और इसी तरह के बाइंड तर्क हमेशा संदर्भ के बजाय एक मान संग्रहीत करते हैं।

3
eerorika 11 अगस्त 2020, 08:43

अगर ऐसा कुछ होता है, तो यह सामान्य रूप से है क्योंकि आपने अपने कार्यक्रम में कहीं न कहीं अपरिभाषित व्यवहार किया है। संकलक ने इसका पता लगाया और जब आक्रामक रूप से अनुकूलन किया गया तो परिणामस्वरूप आपका पूरा कार्यक्रम दूर हो जाएगा।

आपके ठोस उदाहरण में, आपको पहले से ही एक संकेत मिलता है कि संकलक चेतावनी के रूप में कुछ बिल्कुल सही नहीं है:

<source>: In function 'int main()':
<source>:45:18: warning: '<anonymous>' is used uninitialized [-Wuninitialized]
   45 |         return a+b;
      |                  ^

यह कैसे हो सकता है? इस बिंदु पर b के अप्रारंभीकृत होने का क्या कारण हो सकता है?

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

auto x = [&p_argn...]( lambda_t& pp_lambda ) mutable -> decltype(auto) {
    return pp_lambda( std::forward<decltype(p_argn)>(p_argn) ... );
};

b के लिए बाध्य तर्क को पैरामीटर पैक p_argn के रूप में पारित किया जाता है। लेकिन उस पैरामीटर पैक के जीवनकाल पर ध्यान दें: यह संदर्भ द्वारा कब्जा कर लिया गया है! इसलिए यहां कोई पूर्ण अग्रेषण नहीं चल रहा है, इस तथ्य के बावजूद कि आपने लैम्ब्डा बॉडी में std::forward लिखा है, क्योंकि आप लैम्ब्डा में संदर्भ द्वारा कैप्चर करते हैं, और लैम्ब्डा यह नहीं देखता है कि उसके शरीर के बाहर क्या होता है। आसपास का समारोह। a के साथ भी आपको वही जीवन भर की समस्या मिलती है, लेकिन किसी कारण से, संकलक उस के बारे में शिकायत नहीं करना चुनता है। यह आपके लिए अपरिभाषित व्यवहार है, इसकी कोई गारंटी नहीं है कि आपको इसके लिए चेतावनी मिलेगी। इसे ठीक करने का सबसे तेज़ तरीका केवल मूल्य के आधार पर तर्कों को पकड़ना है। आप कुछ अजीबोगरीब सिंटैक्स के साथ नामित कैप्चर का उपयोग करके सही अग्रेषण संपत्ति को बनाए रख सकते हैं:

auto x = [...p_argn = std::forward<decltype(p_argn)>(p_argn)]( lambda_t& pp_lambda ) mutable -> decltype(auto) {
    return pp_lambda(std::move(p_argn)... );
};

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

4
ComicSansMS 11 अगस्त 2020, 08:29

आपको कंपाइलर से कुछ बड़े संकेत मिले हैं:

<source>: In function 'int main()':

<source>:45:18: warning: '<anonymous>' is used uninitialized in this function [-Wuninitialized]

   45 |         return a+b;

      |                  ^

<source>:45:18: warning: '<anonymous>' is used uninitialized in this function [-Wuninitialized]

ASM generation compiler returned: 0

मुद्दा यह है कि आप संदर्भ द्वारा अपनी तर्क सूची (10, 3) को कैप्चर करते हैं, लेकिन कैप्चर किए जाने पर वे अस्थायी मान होते हैं। यदि आप या तो मूल्य से कब्जा करते हैं या वास्तविक चर पास करते हैं, तो कोड त्रुटियों के बिना संकलित होता है और मुझे अपेक्षित परिणाम मिलता है।

आपके सभी कोड को "डिलीट" करने का कारण यह है कि gcc और क्लैंग दोनों ही यह समझने के लिए पर्याप्त स्मार्ट हैं कि आप इसे दो नंबर एक साथ जोड़ने के लिए कह रहे हैं, इसलिए उन्होंने आपके लगभग पूरे प्रोग्राम को ऑप्टिमाइज़ कर दिया है। अंत में विधानसभा इस तरह दिखती है:

main:
        mov     eax, 13
        ret
0
Stephen Newell 11 अगस्त 2020, 08:32