c++ - I=v[i+++] अपरिभाषित क्यों है?




language-lawyer (6)

C ++ (C ++ 11) मानक, .91.9.15 से जो मूल्यांकन के आदेश पर चर्चा करता है, निम्नलिखित कोड उदाहरण है:

void g(int i, int* v) {
    i = v[i++]; // the behavior is undefined
}

जैसा कि कोड नमूने में उल्लेख किया गया है, व्यवहार अपरिभाषित है।

(नोट: थोड़ा अलग निर्माण i + i++ के साथ एक और प्रश्न का उत्तर, क्यों a = i + i ++ अपरिभाषित है और अनिर्दिष्ट व्यवहार नहीं है , यहां लागू हो सकता है: उत्तर अनिवार्य रूप से यह है कि व्यवहार ऐतिहासिक कारणों से अपरिभाषित है, और बाहर नहीं आवश्यकता का। हालांकि, मानक इसके लिए कुछ औचित्य साबित करने के लिए अपरिभाषित लगता है - नीचे दिए गए उद्धरण को देखें। इसके अलावा, यह जुड़ा हुआ प्रश्न समझौते को इंगित करता है कि व्यवहार अनिर्दिष्ट होना चाहिए, जबकि इस प्रश्न में मैं पूछ रहा हूं कि व्यवहार ठीक क्यों नहीं है- निर्दिष्ट ।)

अपरिभाषित व्यवहार के लिए मानक द्वारा दिया गया तर्क निम्नानुसार है:

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

इस उदाहरण में, मुझे लगता है कि उपप्राथमिकता i++ को उपप्राथमिकता v[...] का मूल्यांकन करने से पहले पूरी तरह से मूल्यांकन किया जाएगा, और यह कि उपप्रकारक के मूल्यांकन का परिणाम है i (वेतन वृद्धि से पहले), लेकिन यह है कि i का मान है उसके बाद का उप-मूल्य में वृद्धि का पूरी तरह से मूल्यांकन किया गया है। मुझे लगता है कि उस बिंदु पर (उपप्रकार के बाद i++ का पूरी तरह से मूल्यांकन किया गया है), मूल्यांकन v[...] जगह लेता है, इसके बाद असाइनमेंट i = ...

इसलिए, हालांकि i का वेतन वृद्धि व्यर्थ है, फिर भी मैं यह सोचूंगा कि इसे परिभाषित किया जाना चाहिए।

यह अपरिभाषित व्यवहार क्यों है?


इस उदाहरण में, मुझे लगता है कि उपप्राथमिकता i ++ को उपप्राथमिकता v [...] का मूल्यांकन करने से पहले पूरी तरह से मूल्यांकन किया जाएगा, और यह कि उपप्रकारक के मूल्यांकन का परिणाम है I (वेतन वृद्धि से पहले), लेकिन यह है कि i का मान है उसके बाद का उप-मूल्य में वृद्धि का पूरी तरह से मूल्यांकन किया गया है।

i++ में वृद्धि को v अनुक्रमित करने से पहले मूल्यांकन किया जाना चाहिए और इस तरह i को असाइन करने से पहले, लेकिन उस वेतन वृद्धि के मान को मेमोरी में वापस रखने से पहले नहीं होना चाहिए। बयान में i = v[i++] दो उपविभाजक हैं जो i संशोधित करते हैं (अर्थात एक रजिस्टर से एक स्टोर को चर i में पैदा करने का कारण होगा)। i++ का एक्सप्रेशन x=i+1 , i=x बराबर है, और इसमें कोई आवश्यकता नहीं है कि दोनों संचालन को अनिवार्य रूप से करने की आवश्यकता है:

x = i+1;
y = v[i];
i = y;
i = x;

उस विस्तार के साथ, i का परिणाम v[i] के मूल्य से असंबंधित है। एक अलग विस्तार पर, i = x असाइनमेंट i = y असाइनमेंट से पहले हो सकता है, और परिणाम i = v[i]


मुझे लगता है कि सब-डेक्सप्रेशन v ++ का मूल्यांकन पूरी तरह से मूल्यांकन करने से पहले होगा

लेकिन आप ऐसा क्यों सोचेंगे?

इस कोड के UB होने का एक ऐतिहासिक कारण संकलक अनुकूलन को अनुक्रम बिंदुओं के बीच कहीं भी साइड-इफेक्ट को स्थानांतरित करने की अनुमति देना है। कम अनुक्रम बिंदु, अनुकूलन करने के लिए अधिक संभावित अवसर लेकिन अधिक भ्रमित प्रोग्रामर। यदि कोड कहता है:

a = v[i++];

मानक का आशय यह है कि उत्सर्जित कोड हो सकता है:

a = v[i];
++i;

जहाँ दो निर्देश हो सकते हैं:

tmp = i;
++i;
a = v[tmp];

दो से अधिक होगा।

जब i होता है तो "अनुकूलित कोड" टूट जाता है, लेकिन मानक किसी भी तरह से अनुकूलन की अनुमति देता है , यह कहते हुए कि मूल कोड का व्यवहार जब i होता है तो अपरिभाषित होता है।

मानक आसानी से कह सकता है कि i++ असाइनमेंट से पहले मूल्यांकन किया जाना चाहिए जैसा कि आप सुझाव देते हैं। तब व्यवहार पूरी तरह से परिभाषित किया जाएगा और अनुकूलन निषिद्ध होगा। लेकिन ऐसा नहीं है कि C और C ++ व्यवसाय कैसे करते हैं।

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

void g(int *i, int* v, int *dst) {
    *dst = v[(*i)++];
}

इस फ़ंक्शन के व्यवहार को तब परिभाषित किया गया है जब i != dst , और उस स्थिति में आप वह सभी अनुकूलन चाहते हैं जो आप प्राप्त कर सकते हैं (यही कारण है कि C99 restrict , C89 या C ++ की तुलना में अधिक अनुकूलन की अनुमति देता है)। आपको अनुकूलन देने के लिए, i == dst जब व्यवहार अपरिभाषित होता है। सी और सी ++ मानक एक ठीक रेखा को फैलाते हैं जब यह अलियासिंग की बात आती है, अपरिभाषित व्यवहार के बीच जो प्रोग्रामर द्वारा अपेक्षित नहीं है, और कुछ मामलों में विफल होने वाले वांछनीय अनुकूलन को मना करता है। एसओ पर इसके बारे में प्रश्नों की संख्या बताती है कि प्रश्नकर्ता थोड़ा कम अनुकूलन और थोड़ा अधिक परिभाषित व्यवहार पसंद करेंगे, लेकिन अभी भी रेखा खींचना सरल नहीं है।

इसके अलावा कि क्या व्यवहार पूरी तरह से परिभाषित है, यह मुद्दा है कि क्या यह यूबी होना चाहिए, या उप-अभिव्यक्तियों के अनुरूप कुछ अच्छी तरह से परिभाषित संचालन के निष्पादन का अनिर्दिष्ट आदेश। सी, यूबी के लिए कारण सभी अनुक्रम बिंदुओं के विचार के साथ करना है, और यह तथ्य कि संकलक को वास्तव में एक संशोधित वस्तु के मूल्य की धारणा नहीं है, अगले अनुक्रम बिंदु तक। इसलिए आशावादी को यह कहकर विवश करें कि "" मान किसी अनिर्दिष्ट बिंदु पर परिवर्तित होता है, मानक सिर्फ यह कहता है (पैराफेरेस के लिए): (1) कोई भी कोड जो अगले अनुक्रम बिंदु से पहले संशोधित वस्तु के मूल्य पर निर्भर करता है, यूबी; (2) किसी भी कोड जो संशोधित वस्तु को संशोधित करता है, उसमें यूबी है। जहाँ एक "संशोधित वस्तु" वह कोई भी वस्तु है जिसे उप-अनुक्रमों के मूल्यांकन के कानूनी आदेशों के अंतिम अनुक्रम बिंदु के बाद से संशोधित किया गया होगा

अन्य भाषाएँ (जैसे जावा) पूरे रास्ते जाती हैं और अभिव्यक्ति के दुष्प्रभावों को पूरी तरह से परिभाषित करती हैं, इसलिए निश्चित रूप से सी के दृष्टिकोण के खिलाफ एक मामला है। C ++ सिर्फ उस मामले को स्वीकार नहीं करता है।


इसका कारण सिर्फ ऐतिहासिक नहीं है। उदाहरण:

int f(int& i0, int& i1) {
    return i0 + i1++;
}

अब, इस कॉल के साथ क्या होता है:

int i = 3;
int j = f(i, i);

यह निश्चित रूप से संभव है कि कोड पर आवश्यकताओं को f जाए ताकि इस कॉल का परिणाम अच्छी तरह से परिभाषित हो (जावा ऐसा करता है), लेकिन सी और सी ++ बाधाएं नहीं डालते हैं; यह आशावादियों को अधिक स्वतंत्रता देता है।


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

extern int *foo(void);
extern int *p;

*p = *foo();
*foo() = *p;

यदि बाईं ओर सबस्क्रिप्ट के दाईं ओर और दाईं ओर के मान का मूल्यांकन नहीं किया जाता है, तो दो फ़ंक्शन कॉल को संसाधित करने के सबसे कुशल तरीके कुछ इस तरह होंगे:

[For *p = *foo()]
call foo (which yields result in r0 and trashes r1)
load r0 from address held in r0
load r1 from address held in p
store r0 to address held in r1

[For *foo() = *p]
call foo (which yields result in r0 and trashes r1)
load r1 from address held in p
load r1 from address held in r1
store r1 to address held in r0

किसी भी स्थिति में, यदि p या * p को फू करने के लिए कॉल करने से पहले एक रजिस्टर में पढ़ा जाता है, तो जब तक कि "foo" उस रजिस्टर को परेशान न करने का वादा करता है, "फू" कहने से पहले कंपाइलर को इसके मूल्य को बचाने के लिए एक अतिरिक्त कदम जोड़ने की आवश्यकता होगी। , और बाद में मूल्य को बहाल करने के लिए एक और अतिरिक्त कदम। एक रजिस्टर का उपयोग करके उस अतिरिक्त कदम से बचा जा सकता है कि "फू" परेशान नहीं करेगा, लेकिन यह केवल तभी मदद करेगा जब कोई ऐसा रजिस्टर था जो आसपास के कोड द्वारा आवश्यक मान नहीं रखता था।

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


यदि उदाहरण v[++i] आपके तर्क साझा करूंगा, लेकिन चूंकि i++ एक साइड-इफेक्ट के रूप में i संशोधित करता है, इसलिए यह अपरिभाषित है जब मान संशोधित किया जाता है। मानक शायद एक तरह से या किसी अन्य परिणाम को अनिवार्य कर सकता है, लेकिन यह जानने का कोई सही तरीका नहीं है कि i क्या होना चाहिए: (i + 1) या (v[i + 1])


वहाँ दो नियम।

पहला नियम कई राइट्स के बारे में है जो एक "राइट-राइट खतरा" को जन्म देते हैं: एक ही ऑब्जेक्ट को एक से अधिक बार संशोधित नहीं किया जा सकता है।

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

i++ + i++ और आपकी अभिव्यक्ति i = v[i++] जैसी अभिव्यक्तियाँ पहले नियम का उल्लंघन करती हैं। वे एक वस्तु को दो बार संशोधित करते हैं।

i + i++ जैसी अभिव्यक्ति दूसरे नियम का उल्लंघन करती है। बाईं ओर के उपसर्ग i अपने नए मूल्य की गणना में शामिल किए बिना एक संशोधित वस्तु के मूल्य को देखता है।

तो, i = v[i++] i + i++ (खराब रीड-राइट) से एक अलग नियम (खराब लिखना-लिखना) का उल्लंघन करता है।

नियम बहुत सरलीकृत हैं, जो अजीब अभिव्यक्ति के वर्गों को जन्म देता है। इस पर विचार करो:

p = p->next = q

ऐसा प्रतीत होता है कि एक डेटा प्रवाह निर्भरता है जो खतरों से मुक्त है: असाइनमेंट p = तब तक नहीं हो सकता जब तक कि नया मूल्य ज्ञात नहीं हो जाता। नया मान p->next = q का परिणाम है। मान q "आगे दौड़" नहीं होना चाहिए और p अंदर जाना चाहिए, जैसे कि p->next प्रभावित होता है।

फिर भी, यह अभिव्यक्ति दूसरा नियम तोड़ती है: p को संशोधित किया गया है, और इसका उपयोग इसके नए मूल्य के अभिकलन से संबंधित नहीं के उद्देश्य के लिए भी किया जाता है, अर्थात् भंडारण स्थान का निर्धारण जहां q का मान रखा जाता है!

इसलिए, पूरी तरह से, संकलक को आंशिक रूप से p->next = q का मूल्यांकन करने की अनुमति दी जाती है ताकि यह निर्धारित किया जा सके कि परिणाम q , और इसे p में स्टोर करें, और फिर वापस जाकर p->next = असाइनमेंट पूरा करें। या फिर यह ऐसा लगेगा।

यहां एक प्रमुख मुद्दा यह है कि असाइनमेंट एक्सप्रेशन का मूल्य क्या है? C मानक कहता है कि असाइनमेंट एक्सप्रेशन का मान असाइनमेंट के बाद , अंतराल का है। लेकिन यह अस्पष्ट है: इसका अर्थ "अर्थ हो सकता है" जो कि एक बार असाइनमेंट होता है, "एक बार असाइनमेंट होने के बाद" या असाइनमेंट होने के बाद लैवल्यू में देखे जा सकने वाले मान के रूप में "। C ++ में यह शब्द "[i] n सभी मामलों में स्पष्ट किया जाता है, असाइनमेंट को दाएं और बाएं ऑपरेंड्स के मूल्य गणना के बाद और असाइनमेंट एक्सप्रेशन के मूल्य गणना से पहले अनुक्रमित किया जाता है।" p = p->next = q वैध सी ++ प्रतीत होता है, लेकिन संदिग्ध सी।





language-lawyer