C++ 17 में ऑब्जेक्ट के जीवनकाल के बाहर गैर-स्थिर सदस्य फ़ंक्शन को कॉल करना




language-lawyer c++17 (4)

निम्नलिखित प्रोग्राम में C ++ 17 और बाद में अपरिभाषित व्यवहार है?

struct A {
    void f(int) { /* Assume there is no access to *this here */ }
};

int main() {
    auto a = new A;
    a->f((a->~A(), 0));
}

C ++ 17 गारंटी देता है कि कॉल के तर्क का मूल्यांकन करने से पहले a->f को A ऑब्जेक्ट के सदस्य फ़ंक्शन का मूल्यांकन किया जाता है। इसलिए -> से अप्रत्यक्ष अच्छी तरह से परिभाषित है। लेकिन फ़ंक्शन कॉल दर्ज करने से पहले, तर्क का मूल्यांकन किया जाता है और A ऑब्जेक्ट के जीवनकाल को समाप्त करता है (हालांकि नीचे दिए गए संपादन देखें)। क्या अभी भी कॉल में अपरिभाषित व्यवहार नहीं है? क्या इस तरह से अपने जीवनकाल के बाहर किसी वस्तु के सदस्य को बुलाना संभव है?

a->f का मूल्य वर्ग [basic.life]/7 6.3.2 द्वारा [expr.ref]/6.3.2 और [basic.life]/7 केवल आजीवन वस्तु का संदर्भ देते हुए गैर-स्थैतिक सदस्य फ़ंक्शन कॉल को अस्वीकार करता है। क्या इसका मतलब यह है कि कॉल वैध है? (संपादित करें: जैसा कि टिप्पणियों में चर्चा की गई है कि मुझे गलतफहमी होने की संभावना है [basic.life] / 7 और यह शायद यहाँ लागू होता है)

यदि मैं विध्वंसक कॉल को प्रतिस्थापित करता हूं तो उत्तर बदल जाता a->~A() को delete a साथ या new(a) A ( #include<new> ) के साथ?

मेरे प्रश्न पर कुछ विस्तृत संपादन और स्पष्टीकरण:

यदि मुझे सदस्य फ़ंक्शन कॉल और डिस्ट्रक्टर / डिलीट / प्लेसमेंट-न्यू को दो स्टेटमेंट में अलग करना था, तो मुझे लगता है कि उत्तर स्पष्ट हैं:

  1. a->A(); a->f(0) a->A(); a->f(0) : UB, क्योंकि गैर-स्थैतिक सदस्य अपने जीवनकाल के बाहर कॉल करते हैं। (नीचे संपादित देखें, हालांकि)
  2. delete a; a->f(0) delete a; a->f(0) : ऊपर जैसा
  3. new(a) A; a->f(0) new(a) A; a->f(0) : अच्छी तरह से परिभाषित, नई वस्तु पर कॉल करें

हालाँकि इन सभी मामलों a->f को पहले संबंधित कथन के बाद अनुक्रमित किया जाता है, जबकि यह आदेश मेरे प्रारंभिक उदाहरण में उलटा है। मेरा सवाल यह है कि क्या यह उलट जवाब बदलने की अनुमति देता है?

C ++ 17 से पहले के मानकों के लिए, मैं शुरू में यद्यपि सभी तीन मामलों में अपरिभाषित व्यवहार का कारण बनता हूं, क्योंकि पहले से ही a->f का मूल्यांकन a के मूल्य पर निर्भर करता a , लेकिन तर्क के मूल्यांकन के सापेक्ष परिणामहीन है जो एक साइड-इफेक्ट का कारण बनता है a पर। हालाँकि यह केवल अपरिभाषित व्यवहार है यदि कोई स्केलर वैल्यू पर वास्तविक साइड-इफेक्ट होता है, जैसे कि एक स्केलर ऑब्जेक्ट पर लिखना। हालाँकि, कोई अदिश वस्तु नहीं लिखी गई है क्योंकि A तुच्छ है और इसलिए मुझे इस बात में भी दिलचस्पी होगी कि C ++ 17 से पहले मानकों के मामले में, वास्तव में क्या अड़चन है। विशेष रूप से प्लेसमेंट-नया वाला मामला अब मेरे लिए अस्पष्ट है।

मुझे बस एहसास हुआ कि वस्तुओं के जीवनकाल के संबंध में शब्द C ++ 17 और वर्तमान मसौदे के बीच बदल गए। N4659 में (C ++ 17 ड्राफ्ट) [basic.life] / 1 कहता है:

टाइप T के किसी ऑब्जेक्ट o का जीवनकाल कब समाप्त होता है:

  • यदि T एक गैर-तुच्छ विध्वंसक (15.4) के साथ एक वर्ग प्रकार है, तो विध्वंसक कॉल शुरू होता है

[...]

जबकि वर्तमान मसौदा कहता है:

टाइप T के किसी ऑब्जेक्ट o का जीवनकाल कब समाप्त होता है:

[...]

  • यदि T एक वर्ग प्रकार है, तो विध्वंसक कॉल प्रारंभ होता है, या

[...]

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

यदि f उसके शरीर में *this पहुँचता है, तो जाहिर तौर पर विध्वंसक कॉल और डिलीट एक्सप्रेशन के मामलों के लिए अपरिभाषित व्यवहार हो सकता है, हालांकि इस सवाल में मैं इस बात पर ध्यान केंद्रित करना चाहता हूं कि कॉल अपने आप में मान्य है या नहीं। हालाँकि, ध्यान दें कि प्लेसमेंट-नए के साथ मेरे प्रश्न की भिन्नता संभवतः f में सदस्य के उपयोग के साथ कोई समस्या नहीं होगी, यह इस बात पर निर्भर करता है कि कॉल स्वयं अपरिभाषित व्यवहार है या नहीं। लेकिन उस मामले में विशेष रूप से प्लेसमेंट-नए के मामले के लिए एक अनुवर्ती प्रश्न हो सकता है क्योंकि यह मेरे लिए स्पष्ट नहीं है, कि क्या this फ़ंक्शन में तब हमेशा स्वचालित रूप से नई वस्तु को संदर्भित करेगा या क्या इसे संभवतः std::launder होने की आवश्यकता हो सकती है std::launder एड (यह निर्भर करता है कि सदस्यों के पास क्या है)।

जबकि A में एक तुच्छ विध्वंसक होता है, अधिक दिलचस्प मामला संभवतः ऐसा होता है जहां इसका कुछ दुष्प्रभाव होता है जिसके बारे में कंपाइलर अनुकूलन उद्देश्यों के लिए धारणा बनाना चाहते हैं। (मुझे नहीं पता कि कोई कंपाइलर वास्तव में ऐसा कुछ उपयोग करता है।) इसलिए, मैं उस मामले के लिए जवाब का स्वागत करता हूं जहां A पास गैर-तुच्छ विध्वंसक है, खासकर यदि उत्तर दो मामलों के बीच भिन्न होता है।

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

इस प्रश्न का उद्देश्य भाषा के विवरण की बेहतर समझ प्राप्त करना है। मैं किसी को भी इस तरह कोड लिखने के लिए प्रोत्साहित नहीं करता।


आपको लगता है कि a->f(0) में ये चरण हैं (उस क्रम में हाल के C ++ मानक के लिए, पिछले संस्करणों के लिए कुछ तार्किक क्रम में):

  • मूल्यांकन *a
  • a->f (एक तथाकथित बाउंड मेंबर फंक्शन) का मूल्यांकन
  • मूल्यांकन करना 0
  • तर्क सूची पर बंधे सदस्य a->f को कॉल करना (0)

लेकिन a->f कोई मूल्य या प्रकार नहीं है। यह अनिवार्य रूप से एक गैर-चीज़ है , एक अर्थहीन वाक्यविन्यास तत्व की आवश्यकता केवल इसलिए है क्योंकि व्याकरण सदस्य के उपयोग और फ़ंक्शन कॉल को भी रद्द कर देता है, यहां तक ​​कि सदस्य फ़ंक्शन कॉल पर भी जो सदस्य की पहुंच और फ़ंक्शन कॉल को परिभाषित करता है

इसलिए यह पूछे जाने पर कि जब a->f का "मूल्यांकन" किया जाता है, एक व्यर्थ प्रश्न है: a->f value-less, type-less अभिव्यक्ति के लिए अलग मूल्यांकन चरण जैसी कोई चीज नहीं है

अतः गैर इकाई के मूल्यांकन के क्रम की ऐसी चर्चाओं के आधार पर कोई तर्क भी शून्य और अशक्त है।

संपादित करें:

वास्तव में यह जितना मैंने लिखा है उससे कहीं अधिक बुरा है, अभिव्यक्ति- a->f में एक फोनी "प्रकार" है:

E1.E2 "पैरामीटर प्रकार-सूची cv रिटर्निंग T का कार्य" है।

"फंक्शन ऑफ़ पैरामीटर-टाइप-लिस्ट cv" भी ऐसा कुछ नहीं है जो एक वर्ग के बाहर एक वैध घोषणाकर्ता होगा: एक वैश्विक घोषणा में एक f() const रूप में f() const नहीं हो सकता है:

int ::f() const; // meaningless

और एक वर्ग के अंदर f() const अर्थ "पैरामीटर-प्रकार-सूची का कार्य नहीं है = () cv = const के साथ", इसका अर्थ है सदस्य-फ़ंक्शन (पैरामीटर-प्रकार-सूची = () के साथ cv = const)। "पैरामीटर-प्रकार-सूची cv के फ़ंक्शन" के उचित के लिए कोई उचित घोषणाकर्ता नहीं है। यह केवल एक वर्ग के अंदर ही मौजूद हो सकता है। कोई भी प्रकार का "पैरामीटर-प्रकार-सूची cv रिटर्निंग T" का कार्य नहीं है जिसे घोषित किया जा सकता है या वह वास्तविक कम्प्यूटेशनल है भाव हो सकते हैं।


उपसर्ग अभिव्यक्ति a->f का किसी भी तर्कों के मूल्यांकन से पहले अनुक्रम किया जाता है (जो एक दूसरे के सापेक्ष अनिश्चित रूप से अनुक्रमित होते हैं)। (देखें [expr.call])

तर्कों का मूल्यांकन कार्य के शरीर से पहले किया जाता है (यहां तक ​​कि इनलाइन फ़ंक्शन, देखें [intro.execution])

निहितार्थ, तब यह है कि फ़ंक्शन को कॉल करना अपरिभाषित व्यवहार नहीं है। हालाँकि, किसी भी सदस्य चर को एक्सेस करना या अन्य सदस्य कार्यों को कॉल करना UB प्रति [basic.life] होगा।

इसलिए निष्कर्ष यह है कि यह विशिष्ट उदाहरण शब्दांकन के अनुसार सुरक्षित है, लेकिन सामान्य रूप से एक खतरनाक तकनीक है।


मैं एक भाषा वकील नहीं हूं, लेकिन मैंने आपका कोड स्निपेट लिया और इसे थोड़ा संशोधित किया। मैं इसे उत्पादन कोड में उपयोग नहीं करूंगा, लेकिन यह मान्य परिभाषित परिणाम का उत्पादन करने के लिए लगता है ...

#include <iostream>
#include <exception>

struct A {
    int x{5};
    void f(int){}
    int g() { std::cout << x << '\n'; return x; }
};

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g()));
    catch(const std::exception& e) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

मैं विजुअल स्टूडियो 2017 CE को संकलक भाषा के ध्वज सेट /std:c++latest साथ चला रहा हूं और मेरा IDE का संस्करण 15.9.16 और मुझे फॉलो कंसोल आउटपुट और एग्जिट प्रोग्राम स्टेटस मिलता है:

कंसोल आउटपुट

5

आईडीई निकास स्थिति आउटपुट

The program '[4128] Test.exe' has exited with code 0 (0x0).

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

आइए एक और मामूली संशोधन की कोशिश करें:

#include <iostream>
#include <exception>

struct A {
    int x{5};
    void f(int){}
    int g(int y) { x+=y; std::cout << x << '\n'; return x; }
};

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g(3)));
    catch(const std::exception& e) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

कंसोल आउटपुट

8

आईडीई निकास स्थिति आउटपुट

The program '[4128] Test.exe' has exited with code 0 (0x0).

इस बार चलो अब क्लास नहीं बदलते हैं, लेकिन चलो एक सदस्य को बाद में कॉल करते हैं ...

int main() {
    try {
        auto a = new A;
        a->f((a->~A(), a->g(3)));
        a->g(2);
    } catch( const std::exception& e ) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

कंसोल आउटपुट

8
10

आईडीई निकास स्थिति आउटपुट

The program '[4128] Test.exe' has exited with code 0 (0x0).

यहाँ यह प्रतीत होता है कि a->~A() बाद से ax अपना मूल्य बनाए रख रही है, क्योंकि new को A पर बुलाया गया था और अभी तक delete नहीं किया गया है।

इससे भी अधिक अगर मैं new निकालता हूं और आवंटित डायनामिक हीप मेमोरी के बजाय स्टैक पॉइंटर का उपयोग करता हूं:

int main() {
    try {
        A b;
        A* a = &b;    
        a->f((a->~A(), a->g(3)));
        a->g(2);
    } catch( const std::exception& e ) {
        std::cerr << e.what();
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}

मुझे अभी भी मिल रहा है:

कंसोल आउटपुट

8
10

आईडीई निकास स्थिति आउटपुट

जब मैं अपने संकलक की भाषा ध्वज सेटिंग को /c:std:c++latest /std:c++17 बदलता हूं तो मुझे वही सटीक परिणाम मिल रहे हैं।

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

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


यह सच है कि तुच्छ विध्वंसक कुछ भी नहीं करते हैं, वस्तु के जीवनकाल को समाप्त नहीं करते हैं, इससे पहले (+ के लिए योजनाएं) ++ 20। तो सवाल यह है कि एर, तुच्छ तब तक जब तक कि हम एक गैर-तुच्छ विध्वंसक या कुछ और मजबूत न करें जैसे कि delete

उस स्थिति में, C ++ 17 का आदेश मदद नहीं करता है: कॉल (क्लास सदस्य का उपयोग नहीं) ऑब्जेक्ट के लिए एक पॉइंटर का उपयोग करता है ( इसे शुरू this ), आउट-ऑफ-लाइफ पॉइंट के लिए नियमों का उल्लंघन करते हुए

साइड नोट: यदि केवल एक आदेश अपरिभाषित था, तो C ++ 17 से पहले "अनिर्दिष्ट आदेश" होगा: यदि अनिर्दिष्ट व्यवहार के लिए संभावनाओं में से कोई भी अपरिभाषित व्यवहार है, तो व्यवहार अपरिभाषित है। (आप कैसे बताएंगे कि अच्छी तरह से परिभाषित विकल्प चुना गया था? अपरिभाषित व्यक्ति इसका अनुकरण कर सकता है और फिर नक्सल राक्षसों को छोड़ सकता है।)





order-of-execution