क्या किसी संरचना में अनुक्रमण करना कानूनी है?


104

भले ही कोड कितना 'बुरा' हो, और यह मानते हुए कि संरेखण आदि संकलक / मंच पर कोई मुद्दा नहीं है, क्या यह अपरिभाषित या टूटा हुआ व्यवहार है?

अगर मेरे पास इस तरह की एक संरचना है: -

struct data
{
    int a, b, c;
};

struct data thing;

यह है कानूनी उपयोग करने के लिए a, bऔर cके रूप में (&thing.a)[0], (&thing.a)[1]है, और (&thing.a)[2]?

हर मामले में, हर कंपाइलर और प्लेटफ़ॉर्म पर मैंने इसे आज़माया, हर सेटिंग के साथ मैंने इसे 'काम' करने की कोशिश की। मुझे बस इस बात की चिंता है कि कंपाइलर को एहसास नहीं हो सकता है कि बी और चीज [1] एक ही चीज हैं और 'बी' के स्टोर एक रजिस्टर में रखे जा सकते हैं और चीज [1] मेमोरी से गलत वैल्यू (उदाहरण के लिए) पढ़ती है। हर मामले में मैंने कोशिश की, हालांकि यह सही था। (मुझे निश्चित रूप से पता है कि ज्यादा साबित नहीं होता है)

यह मेरा कोड नहीं है; यह कोड मुझे काम करना है, मुझे इसमें दिलचस्पी है कि क्या यह खराब कोड या टूटा हुआ कोड है क्योंकि अलग-अलग तरीकों से इसे बदलने के लिए मेरी प्राथमिकताएं प्रभावित होती हैं :)

टैग की गईं सी और सी ++। मैं ज्यादातर सी ++ में दिलचस्पी रखता हूं, लेकिन सी भी अगर यह अलग है, तो बस ब्याज के लिए।


51
नहीं, यह "कानूनी" नहीं है। यह अपरिभाषित व्यवहार है।
सैम वार्शविक

10
यह आपके लिए इस बहुत ही सरल मामले में काम करता है क्योंकि संकलक सदस्यों के बीच कोई पैडिंग नहीं जोड़ता है। अलग-अलग आकार के प्रकारों का उपयोग करके संरचनाओं के साथ प्रयास करें और दुर्घटनाग्रस्त हो जाएंगे।
कुछ प्रोग्रामर ने

7
अतीत को खोदना - यूबी निक- नेसल डेमोंस हुआ करता था
एड्रियन कोलोमिची

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

7
यदि आपको तत्वों को एक सरणी के रूप में एक्सेस करने की आवश्यकता है, तो उन्हें एक सरणी के रूप में परिभाषित करें। यदि उन्हें अलग-अलग नाम रखने हों, तो नामों का उपयोग करें। अपना केक रखने और इसे खाने की कोशिश करने से अंततः अपच हो सकती है - शायद सबसे असुविधाजनक कल्पनाशील समय पर। (मुझे लगता है कि सूचकांक 0 सी में कानूनी है, सूचकांक 1 या 2 नहीं है। ऐसे संदर्भ हैं जिनमें एक तत्व को आकार 1. की एक सरणी के रूप में माना जाता है)
जोनाथन लेफ़लर

जवाबों:


73

यह गैरकानूनी है 1 । यह C ++ में एक अपरिभाषित व्यवहार है।

आप सदस्यों को एक सरणी फैशन में ले जा रहे हैं, लेकिन यहाँ वही है जो C ++ मानक कहता है (जोर मेरा):

[dcl.array / 1] : ... सरणी प्रकार की एक वस्तु मेंT के प्रकार के एन सबोबजेक्ट केएक सन्निहित रूप से आवंटित गैर-रिक्त सेट होता है ...

लेकिन, सदस्यों के लिए, ऐसी कोई सन्निहित आवश्यकता नहीं है:

[class.mem / 17] : ...; कार्यान्वयन संरेखण आवश्यकताओं में दो आसन्न सदस्यों को प्रत्येक के तुरंत बाद आवंटित नहीं होने का कारण हो सकता है ...

जबकि उपरोक्त दोनों उद्धरण संकेत करने के लिए पर्याप्त होना चाहिए कि structआपने सी + + मानक द्वारा एक परिभाषित व्यवहार क्यों नहीं किया है, आइए एक उदाहरण चुनें: अभिव्यक्ति को देखें (&thing.a)[2]- सबस्क्रिप्ट ऑपरेटर के बारे में:

[expr.post//expr.sub/1] : वर्ग कोष्ठक में एक अभिव्यक्ति के बाद एक उपसर्ग अभिव्यक्ति एक पोस्टफिक्स अभिव्यक्ति है। भावों में से एक "टी की सरणी" का एक प्रकार या "टी के लिए सूचक" का प्रचलन होगा और दूसरा बिना किसी गणना या अभिन्न प्रकार का एक प्रचलन होगा। परिणाम "टी" प्रकार का है। प्रकार "T" पूरी तरह से परिभाषित वस्तु प्रकार होगा ।.66 अभिव्यक्ति E1[E2]समान (परिभाषा के अनुसार) है((E1)+(E2))

उपरोक्त उद्धरण के बोल्ड टेक्स्ट में खुदाई: एक सूचक प्रकार से अभिन्न प्रकार को जोड़ने के बारे में (यहाँ जोर पर ध्यान दें) ।।

[expr.add / 4] : जब एक प्रकार का अभिन्न अंग जो एक सूचक से जोड़ा या घटाया जाता है, तो परिणाम में सूचक संकार का प्रकार होता है। यदि अभिव्यक्ति n तत्वों के साथ एक सरणी ऑब्जेक्ट केP तत्वx[i]कोइंगितकरता है, तो भाव(औरजहांमूल्य है) (संभवतः-काल्पनिक) तत्व को इंगित करता है यदि; अन्यथा , व्यवहार अपरिभाषित है। ...xP + JJ + PJjx[i + j]0 ≤ i + j ≤ n

यदि क्लॉज के लिए सरणी की आवश्यकता पर ध्यान दें ; बाकी अन्यथा उपर्युक्त उद्धरण में। अभिव्यक्ति स्पष्ट रूप से अगर क्लॉज के लिए योग्य नहीं है ; इसलिए, अपरिभाषित व्यवहार।(&thing.a)[2]


एक साइड नोट पर: हालांकि मैंने विभिन्न कंपाइलरों पर कोड और इसकी विविधताओं का बड़े पैमाने पर प्रयोग किया है और वे यहां कोई भी पैडिंग पेश नहीं करते हैं, (यह काम करता है ); एक रखरखाव दृश्य से, कोड बेहद नाजुक है। आपको अभी भी इस बात पर जोर देना चाहिए कि ऐसा करने से पहले कार्यान्वयन को सदस्यों को आवंटित किया गया था। और सीमा में रहते हैं :-)। लेकिन इसका अभी भी अपरिभाषित व्यवहार ...।

कुछ व्यवहार्य वर्कअराउंड (परिभाषित व्यवहार के साथ) अन्य उत्तरों द्वारा प्रदान किए गए हैं।



जैसा कि टिप्पणियों में ठीक कहा गया है, [basic.lval / 8] , जो मेरे पिछले संपादन में लागू नहीं था। धन्यवाद @ 2501 और @ एमएम

1 : केवल एक कानूनी मामले के लिए इस प्रश्न के लिए @ बैरी का उत्तर देखें जहां आप thing.aइस पार्टटेन के माध्यम से संरचना के सदस्य तक पहुंच सकते हैं ।


1
@jcoder यह class.mem में परिभाषित किया गया है । वास्तविक पाठ के लिए अंतिम पैराग्राफ देखें।
नाथनऑलिवर

4
यहां सख्त सख्त प्रासंगिक नहीं है। प्रकार int कुल प्रकार के भीतर निहित है और यह प्रकार अन्य नाम int हो सकता है। - an aggregate or union type that includes one of the aforementioned types among its elements or non-static data members (including, recursively, an element or non-static data member of a subaggregate or contained union),
2501

1
@ Downvoters, टिप्पणी करने के लिए परवाह है? - और यह उत्तर गलत है या सुधारने के लिए?
WhiZTiM

4
सख्त अलियासिंग इस के लिए अप्रासंगिक है। पैडिंग किसी ऑब्जेक्ट के संग्रहीत मूल्य का हिस्सा नहीं है। इसके अलावा यह उत्तर सबसे आम मामले को संबोधित करने में विफल रहता है: जब कोई पैडिंग नहीं होती है तो क्या होता है। वास्तव में इस उत्तर को हटाने की सिफारिश करेंगे।
एमएम

1
किया हुआ! मैंने सख्ती-अलियासिंग के बारे में पैराग्राफ हटा दिया है।
WhiZTiM

48

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

अपरिभाषित व्यवहार का कारण बनने वाली चीज आउट-ऑफ-बाउंड्स एक्सेस 1 है । जब आपके पास एक अदिश राशि होती है (सदस्य a, b, c, संरचना में) और इसे अगले काल्पनिक तत्व तक पहुंचने के लिए एक सरणी 2 के रूप में उपयोग करने का प्रयास करते हैं , तो आप अपरिभाषित व्यवहार का कारण बनते हैं, भले ही उसी प्रकार का कोई अन्य ऑब्जेक्ट हो वह पता।

हालाँकि आप संरचना वस्तु के पते का उपयोग कर सकते हैं और एक विशिष्ट सदस्य में ऑफसेट की गणना कर सकते हैं:

struct data thing = { 0 };
char* p = ( char* )&thing + offsetof( thing , b );
int* b = ( int* )p;
*b = 123;
assert( thing.b == 123 );

यह प्रत्येक सदस्य के लिए व्यक्तिगत रूप से किया जाना चाहिए, लेकिन एक फ़ंक्शन में रखा जा सकता है जो एक सरणी एक्सेस जैसा दिखता है।


1 (से उद्धृत: ISO / IEC 9899: 201x 6.5.6 Additive ऑपरेटर्स 8)
यदि परिणाम सरणी ऑब्जेक्ट के अंतिम तत्व से एक को इंगित करता है, तो इसका उपयोग मूल्यांकन किए जाने वाले एकरी * ऑपरेटर के ऑपरेटर के रूप में नहीं किया जाएगा।

2 (से उद्धृत: ISO / IEC 9899: 201x 6.5.6 Additive ऑपरेटर्स 7)
इन ऑपरेटरों के उद्देश्यों के लिए, एक ऑब्जेक्ट के लिए एक पॉइंटर जो कि एक एरे का तत्व नहीं है, एक पॉइंटर के पहले तत्व के समान पॉइंटर व्यवहार करता है वस्तु के प्रकार के साथ उसके तत्व प्रकार के रूप में लंबाई का एक सरणी।


3
ध्यान दें कि यह केवल तभी काम करता है जब कक्षा एक मानक लेआउट प्रकार है। यदि नहीं तो भी यह यू.बी.
नाथनऑलिवर

@NathanOliver मुझे इस बात का उल्लेख करना चाहिए कि मेरा उत्तर केवल सी। एडिट पर लागू होता है। यह ऐसे दोहरे टैग भाषा प्रश्नों की समस्याओं में से एक है।
2501

धन्यवाद, और यही कारण है कि मैंने सी ++ और सी के लिए अलग से पूछा क्योंकि यह मतभेदों को जानने के लिए दिलचस्प है
jcoder

@NathanOliver यदि मानक लेआउट है, तो C ++ वर्ग के पते के साथ पहले सदस्य का पता मेल खाने की गारंटी है। हालांकि, यह न तो गारंटी देता है कि पहुंच अच्छी तरह से परिभाषित है और न ही इसका मतलब है कि अन्य वर्गों पर ऐसी पहुंच अपरिभाषित है।
पोटाटोसवेटर

क्या आप कहेंगे कि char* p = ( char* )&thing.a + offsetof( thing , b );अपरिभाषित व्यवहार की ओर जाता है?
एमएम

43

C ++ में यदि आपको वास्तव में इसकी आवश्यकता है - ऑपरेटर बनाएं []:

struct data
{
    int a, b, c;
    int &operator[]( size_t idx ) {
        switch( idx ) {
            case 0 : return a;
            case 1 : return b;
            case 2 : return c;
            default: throw std::runtime_error( "bad index" );
        }
    }
};


data d;
d[0] = 123; // assign 123 to data.a

यह केवल काम करने की गारंटी नहीं है, लेकिन उपयोग सरल है, आपको अपठनीय अभिव्यक्ति लिखने की आवश्यकता नहीं है (&thing.a)[0]

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

struct data 
{
     int array[3];
     int &a = array[0];
     int &b = array[1];
     int &c = array[2];
};

यह समाधान संरचना के आकार को बदल देगा ताकि आप विधियों का उपयोग कर सकें:

struct data 
{
     int array[3];
     int &a() { return array[0]; }
     int &b() { return array[1]; }
     int &c() { return array[2]; }
};

1
मुझे इस प्रकार की नासमझी को देखना पसंद होगा, बनाम सी टाइपिंग का उपयोग करते हुए सी प्रोग्राम के डिसएस्पेशन को। लेकिन, लेकिन ... सी ++ जितनी तेजी से सी ... सही है? सही?
लुंडिन

6
@ लुंडिन यदि आप इस निर्माण की गति के बारे में परवाह करते हैं, तो डेटा को पहले एक सरणी के रूप में व्यवस्थित किया जाना चाहिए, न कि अलग-अलग फ़ील्ड के रूप में।
स्लाव

2
@ लुंडिन आप दोनों में अपठनीय और अपरिभाषित व्यवहार का मतलब है? जी नहीं, धन्यवाद।
स्लाव

1
@ लुंडिन ऑपरेटर ओवरलोडिंग एक संकलन-समय सिंटैक्टिक विशेषता है जो सामान्य कार्यों की तुलना में किसी भी ओवरहेड को प्रेरित नहीं करता है। Godbolt.org/g/vqhREz पर एक नज़र डालें कि संकलक वास्तव में क्या करता है जब वह C ++ और C कोड संकलित करता है। यह आश्चर्यजनक है कि वे क्या करते हैं और उन्हें क्या करने की उम्मीद है। मैं व्यक्तिगत रूप से C ++ से अधिक एक लाख बार C टाइप-सुरक्षा और स्पष्टता पसंद करता हूं। और यह पैडिंग के बारे में मान्यताओं पर भरोसा किए बिना हर समय काम करता है।
जेन्स

2
वे संदर्भ कम से कम चीज के आकार को दोगुना कर देंगे। बस करो thing.a()
TC

14

C ++ के लिए: यदि आपको किसी सदस्य को उसका नाम ज्ञात किए बिना एक्सेस करने की आवश्यकता है, तो आप पॉइंटर से सदस्य चर का उपयोग कर सकते हैं।

struct data {
  int a, b, c;
};

typedef int data::* data_int_ptr;

data_int_ptr arr[] = {&data::a, &data::b, &data::c};

data thing;
thing.*arr[0] = 123;

1
यह भाषा सुविधाओं का उपयोग कर रहा है, और एक परिणाम के रूप में अच्छी तरह से परिभाषित है और, जैसा कि मुझे लगता है, कुशल है। सबसे बढ़िया उत्तर।
पीटर -

2
कुशल मान लें? मैं इसके विपरीत मानता हूं। देखो उत्पन्न कोड पर।
JDługosz

1
@ JDługosz, आप काफी सही हैं। उत्पन्न विधानसभा पर एक नज़रoffsetoff
रखना

3
आप गिरफ्तार कॉन्स्ट्रेक्स बनाकर चीजों को भी सुधार सकते हैं। यह फ्लाई पर बनाने के बजाय डेटा अनुभाग में एक एकल निश्चित लुकअप तालिका बनाएगा।
टिम

10

ISO C99 / C11 में, यूनियन-आधारित टाइप-पिंगिंग कानूनी है, इसलिए आप इसका उपयोग इंडेक्सिंग पॉइंटर्स के बजाय गैर-सरणियों (विभिन्न अन्य उत्तर देखें) के लिए कर सकते हैं।

ISO C ++ यूनियन-आधारित टाइप-पिंगेट की अनुमति नहीं देता है। GNU C ++ एक एक्सटेंशन के रूप में करता है, और मुझे लगता है कि कुछ अन्य कंपाइलर जो सामान्य रूप से GNU एक्सटेंशन का समर्थन नहीं करते हैं, यूनियन टाइप-पिंगिंग का समर्थन करते हैं। लेकिन इससे आपको सख्ती से पोर्टेबल कोड लिखने में मदद नहीं मिलती है।

Gcc और clang के वर्तमान संस्करणों के साथ, C ++ सदस्य फ़ंक्शन को लिखने के लिए एक सदस्य switch(idx)का चयन करके संकलन-समय निरंतर सूचकांकों के लिए दूर का अनुकूलन होगा, लेकिन रनटाइम सूचकांकों के लिए भयानक ब्रांडी एसम का उत्पादन होगा। इसके लिए कुछ भी गलत नहीं है switch(); यह बस वर्तमान संकलक में एक चूक-अनुकूलन बग है। वे स्लाव 'स्विच () फ़ंक्शन को कुशलतापूर्वक संकलित कर सकते थे।


इसका समाधान / समाधान इसे दूसरे तरीके से करना है: अपनी कक्षा / संरचना को एक सरणी सदस्य दें, और विशिष्ट तत्वों को नाम संलग्न करने के लिए एक्सेसर फ़ंक्शन लिखें।

struct array_data
{
  int arr[3];

  int &operator[]( unsigned idx ) {
      // assert(idx <= 2);
      //idx = (idx > 2) ? 2 : idx;
      return arr[idx];
  }
  int &a(){ return arr[0]; } // TODO: const versions
  int &b(){ return arr[1]; }
  int &c(){ return arr[2]; }
};

हम Godbolt संकलक एक्सप्लोरर पर विभिन्न उपयोग-मामलों के लिए asm आउटपुट पर एक नज़र डाल सकते हैं । ये पूर्ण x86-64 सिस्टम V फ़ंक्शन हैं, अनुगामी RET निर्देश बेहतर दिखाने के लिए छोड़ा गया है कि जब आप इनलाइन होते हैं तो आपको क्या मिलेगा। एआरएम / एमआइपी / जो भी समान होगा।

# asm from g++6.2 -O3
int getb(array_data &d) { return d.b(); }
    mov     eax, DWORD PTR [rdi+4]

void setc(array_data &d, int val) { d.c() = val; }
    mov     DWORD PTR [rdi+8], esi

int getidx(array_data &d, int idx) { return d[idx]; }
    mov     esi, esi                   # zero-extend to 64-bit
    mov     eax, DWORD PTR [rdi+rsi*4]

तुलना करके, switch()C ++ के लिए @ स्लाव का उत्तर रनटाइम-वैरिएबल इंडेक्स के लिए इस तरह एएसएम बनाता है। (पिछले गॉडबोल्ट लिंक में कोड)।

int cpp(data *d, int idx) {
    return (*d)[idx];
}

    # gcc6.2 -O3, using `default: __builtin_unreachable()` to promise the compiler that idx=0..2,
    # avoiding an extra cmov for idx=min(idx,2), or an extra branch to a throw, or whatever
    cmp     esi, 1
    je      .L6
    cmp     esi, 2
    je      .L7
    mov     eax, DWORD PTR [rdi]
    ret
.L6:
    mov     eax, DWORD PTR [rdi+4]
    ret
.L7:
    mov     eax, DWORD PTR [rdi+8]
    ret

यह स्पष्ट रूप से भयानक है, सी (या GNU C ++) की तुलना में संघ-आधारित प्रकार के चालाक संस्करण:

c(type_t*, int):
    movsx   rsi, esi                   # sign-extend this time, since I didn't change idx to unsigned here
    mov     eax, DWORD PTR [rdi+rsi*4]

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

जबकि यूनियन-आधारित टाइप पाइंटिंग []सीधे तौर पर यूनियन सदस्य पर ऑपरेटर का उपयोग करते हुए gcc और क्लैंग में काम करने के लिए लगता है , जबकि मानक array[index]समान होने के रूप में परिभाषित करता है *((array)+(index)), और न ही gcc और न ही क्लैंग मज़बूती से यह पहचान पाएंगे कि एक एक्सेस *((someUnion.array)+(index))एक एक्सेस है someUnion। केवल यही स्पष्टीकरण मैं देख सकता हूं कि someUnion.array[index]*((someUnion.array)+(index))तो मानक द्वारा परिभाषित किया गया है, लेकिन केवल एक लोकप्रिय एक्सटेंशन हैं, और gcc / clang ने दूसरे का समर्थन नहीं करने का विकल्प चुना है, लेकिन कम से कम अब के लिए पहले का समर्थन करते हैं।
सुपरकैट

9

C ++ में, यह ज्यादातर है अपरिभाषित व्यवहार है (यह किस सूचकांक पर निर्भर करता है)।

[Expr.unary.op] से:

सूचक अंकगणितीय (5.7) और तुलना (5.9, 5.10) के उद्देश्यों के लिए, एक वस्तु जो एक सरणी तत्व नहीं है, जिसका पता इस तरह से लिया जाता है, एक प्रकार के तत्व के साथ एक सरणी से संबंधित माना जाता है T

इस &thing.aप्रकार अभिव्यक्ति को एक सरणी के संदर्भ में माना जाता है int

[Expr.sub] से:

अभिव्यक्ति E1[E2]समान (परिभाषा के अनुसार) है*((E1)+(E2))

और [expr.add] से:

जब एक अभिन्न प्रकार का अभिन्न अंग किसी सूचक से जोड़ा या घटाया जाता है, तो परिणाम में सूचक संकारक का प्रकार होता है। यदि अभिव्यक्ति तत्वों के साथ एक सरणी वस्तु के Pतत्व x[i]को इंगित करता है, तो भाव ( और जहां मूल्य है ) (संभवतः-काल्पनिक) तत्व को इंगित करता है यदि ; अन्यथा, व्यवहार अपरिभाषित है।xnP + JJ + PJjx[i + j]0 <= i + j <= n

(&thing.a)[0]यह पूरी तरह से सुव्यवस्थित है क्योंकि &thing.aआकार 1 की एक सरणी मानी जाती है और हम उस पहले सूचकांक को ले रहे हैं। यह लेने के लिए एक अनुमत सूचकांक है।

(&thing.a)[2]पूर्व शर्त का उल्लंघन करती है कि 0 <= i + j <= n, के बाद से हमारे पास i == 0, j == 2, n == 1। बस सूचक &thing.a + 2का निर्माण अपरिभाषित व्यवहार है।

(&thing.a)[1]दिलचस्प मामला है। यह वास्तव में [expr.add] में कुछ भी उल्लंघन नहीं करता है। हमें सरणी के अंत में एक पॉइंटर लेने की अनुमति है - जो यह होगा। यहाँ, हम एक नोट की ओर मुड़ते हैं [basic.compound]:

एक पॉइंटर प्रकार का एक मान जो किसी ऑब्जेक्ट के अंत या उसके पिछले का सूचक होता है, मेमोरी में पहले बाइट का पता दर्शाता है (1.7) ऑब्जेक्ट द्वारा कब्जे में रखा गया या ऑब्जेक्ट द्वारा कब्जा किए गए स्टोरेज के अंत के बाद मेमोरी में पहला बाइट। , क्रमशः। [नोट: ऑब्जेक्ट के अंत में एक पॉइंटर (5.7) को उस प्रकार के ऑब्जेक्ट के असंबंधित ऑब्जेक्ट को इंगित करने के लिए नहीं माना जाता है जो उस पते पर स्थित हो सकता है।

इसलिए, पॉइंटर &thing.a + 1को परिभाषित करना व्यवहार को परिभाषित करता है, लेकिन डेरेफेरिंग यह अपरिभाषित है क्योंकि यह किसी भी चीज की ओर इशारा नहीं करता है।


मूल्यांकन करना (& thing.a) + 1 केवल कानूनी के बारे में है क्योंकि एक सरणी के अंत में एक सूचक पिछले कानूनी है; संग्रहित डेटा को पढ़ना या लिखना अपरिभाषित व्यवहार है, और </>, <=,> = = के साथ तुलना अपरिभाषित व्यवहार है। ((बात। ए) + 2 बिल्कुल अवैध है।
gnasher729

@ gnasher729 हाँ यह जवाब कुछ और स्पष्ट करने लायक है।
बैरी

(&thing.a + 1)एक दिलचस्प मामले मैं कवर करने में विफल रहा है। +1! ... बस उत्सुक, आप आईएसओ सी ++ समिति पर हैं?
WhiZTiM

यह भी एक बहुत महत्वपूर्ण मामला है क्योंकि अन्यथा आधे-खुले अंतराल के रूप में पॉइंटर्स का उपयोग करने वाला प्रत्येक लूप यूबी होगा।
जेन्स

अंतिम मानक प्रशस्ति पत्र के बारे में। C ++ यहां C से बेहतर होना चाहिए।
2501

8

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

C ++ में बहुत सारे नियम हैं जो संकलक को यह समझने का प्रयास करते हैं कि आप क्या कर रहे हैं, इसलिए यह इसके बारे में तर्क कर सकता है और इसे अनुकूलित कर सकता है।

अलियासिंग (दो अलग-अलग पॉइंटर प्रकारों के माध्यम से डेटा तक पहुंच), सरणी सीमा, आदि के बारे में नियम हैं।

जब आपके पास एक चर होता है x, तो यह तथ्य कि यह किसी सरणी का सदस्य नहीं है, इसका मतलब है कि संकलक मान सकता है कि कोई भी []आधारित सरणी पहुंच इसे संशोधित नहीं कर सकती है। इसलिए इसका उपयोग करने पर हर बार मेमोरी से डेटा को लगातार लोड नहीं करना पड़ता है; केवल अगर कोई इसे अपने नाम से संशोधित कर सकता था ।

इस प्रकार (&thing.a)[1]संकलक द्वारा संदर्भित नहीं किया जा सकता है thing.b। यह इस तथ्य का उपयोग रीडर्स को फिर से लिखने और लिखने के लिए कर सकता हैthing.b , यह अमान्य कर सकता है कि आप जो करना चाहते हैं उसे अमान्य किए बिना जो आपने वास्तव में इसे करने के लिए कहा था।

इसका एक उत्कृष्ट उदाहरण है, कब्ज दूर करना।

const int x = 7;
std::cout << x << '\n';
auto ptr = (int*)&x;
*ptr = 2;
std::cout << *ptr << "!=" << x << '\n';
std::cout << ptr << "==" << &x << '\n';

यहाँ आप आमतौर पर एक संकलक कहते हैं 7 तो 2! = 7, और फिर दो समान संकेत; इस तथ्य के बावजूद जो ptrइंगित कर रहा है x। संकलक तथ्य यह xहै कि जब आप के मूल्य के लिए पूछना यह पढ़ने के लिए परेशान नहीं करने के लिए एक निरंतर मूल्य हैx

लेकिन जब आप इसका पता लेते हैं x, तो आप इसे मौजूद होने के लिए मजबूर करते हैं। फिर आप कास्ट को दूर करते हैं, और इसे संशोधित करते हैं। तो स्मृति में वास्तविक स्थान जहां xसंशोधित किया गया है, संकलक वास्तव में इसे पढ़ने के लिए नहीं पढ़ने के लिए स्वतंत्र हैx !

कंपाइलर को यह पता लगाने के लिए पर्याप्त स्मार्ट हो सकता है कि ptrपढ़ने के लिए भी कैसे बचें *ptr, लेकिन अक्सर वे नहीं होते हैं। बेझिझक जाओ और उपयोग करोptr = ptr+argc-1अगर आशावादी आपसे ज्यादा स्मार्ट हो रहा है तो या उसका या सोमसुख भ्रम का अनुभव करें।

आप एक कस्टम प्रदान कर सकते हैं operator[]जो सही आइटम प्राप्त करता है।

int& operator[](std::size_t);
int const& operator[](std::size_t) const;

दोनों का होना उपयोगी है।


"तथ्य यह है कि यह किसी सरणी का सदस्य नहीं है, इसका मतलब है कि संकलक मान सकता है कि कोई [] आधारित सरणी पहुंच इसे संशोधित नहीं कर सकती है।" - सच नहीं है, उदाहरण के (&thing.a)[0]लिए इसे संशोधित कर सकते हैं
एमएम

मैं यह नहीं देखता कि प्रश्न के साथ कास्ट उदाहरण का कोई लेना देना नहीं है। यह केवल इसलिए विफल होता है क्योंकि एक विशिष्ट नियम है कि एक कास्ट ऑब्जेक्ट को संशोधित नहीं किया जा सकता है, किसी अन्य कारण से नहीं।
एमएम

1
@MM, यह एक संरचना में अनुक्रमित करने का एक उदाहरण नहीं है, लेकिन यह एक बहुत अच्छा चित्रण है कि स्मृति में अपने स्पष्ट स्थान द्वारा किसी वस्तु को संदर्भित करने के लिए अपरिभाषित व्यवहार का उपयोग कैसे किया जा सकता है, परिणाम में अपेक्षा से भिन्न उत्पादन हो सकता है, क्योंकि कंपाइलर कुछ के साथ कर सकता है UB से आप इसे चाहते थे।
वाइल्डकार्ड

@MM क्षमा करें, ऑब्जेक्ट के लिए एक सूचक के माध्यम से एक तुच्छ के अलावा अन्य कोई पहुंच नहीं है। और दूसरा एक अपरिभाषित व्यवहार के दुष्प्रभावों को देखने के लिए आसान का एक उदाहरण है; संकलक रीड्स का अनुकूलन करता है xक्योंकि यह जानता है कि आप इसे परिभाषित तरीके से नहीं बदल सकते। इसी प्रकार के अनुकूलन हो सकता है जब आप को बदलने bके माध्यम से (&blah.a)[1]करता है, तो संकलक साबित कर सकते हैं करने के लिए कोई परिभाषित प्रवेश किया गया था bकि यह बदल सकता है; संकलक, आस-पास के कोड, या जो भी हो, में ऐसा प्रतीत होता है कि इस तरह का बदलाव बहुत ही सहज बदलाव के कारण हो सकता है। तो यह भी परीक्षण कि यह काम करता है पर्याप्त नहीं है।
यक्क - एडम नेवेरुमोंट

6

नाम से सदस्य सरणी में तत्वों तक पहुंचने के लिए प्रॉक्सी क्लास का उपयोग करने का एक तरीका है। यह बहुत सी ++ है, और इसमें सिंटैक्टिक वरीयता को छोड़कर रेफरी-रिटर्निंग एक्सेसर फ़ंक्शंस का कोई लाभ नहीं है। यह ->ऑपरेटर को सदस्यों के रूप में तत्वों का उपयोग करने के लिए अधिभारित करता है , इसलिए स्वीकार्य होने के लिए, दोनों को एक्सेसर्स ( d.a() = 5;) के सिंटैक्स को नापसंद करने की आवश्यकता होती है , साथ ही ->गैर-पॉइंटर ऑब्जेक्ट के साथ उपयोग करने को सहन करना पड़ता है । मुझे उम्मीद है कि यह पाठकों को कोड से परिचित न होने के लिए भ्रमित कर सकता है, इसलिए यह कुछ साफ-सुथरी चाल से अधिक हो सकता है जो आप उत्पादन में डालना चाहते हैं।

Dataइस कोड में struct भी अपने अंदर पहुँच अनुक्रमित तत्वों के लिए, सबस्क्रिप्ट ऑपरेटर के लिए भार के शामिल ar, सरणी सदस्य के साथ-साथ beginऔरend काम करता है, यात्रा के लिए। इसके अलावा, इन सभी को गैर-कास्ट और कास्ट संस्करणों के साथ अतिभारित किया जाता है, जो मुझे पूर्णता के लिए शामिल करने की आवश्यकता महसूस हुई।

जब Data's ->नाम से एक तत्व तक पहुंचना है (इस तरह: प्रयोग किया जाता है my_data->b = 5;), एक Proxyवस्तु दिया जाता है। फिर, क्योंकि यह Proxyप्रतिद्वंद्विता पॉइंटर नहीं है, इसका अपना ->ऑपरेटर ऑटो-चेन-कॉल है, जो अपने आप में एक पॉइंटर लौटाता है। इस तरह, Proxyऑब्जेक्ट को त्वरित किया जाता है और प्रारंभिक अभिव्यक्ति के मूल्यांकन के दौरान वैध रहता है।

किसी Proxyऑब्जेक्ट का निर्देश उसके 3 संदर्भ सदस्यों को पॉप्युलेट करता है a, bऔर cकंस्ट्रक्टर में पारित एक पॉइंटर के अनुसार, जिसे एक बफर को इंगित करने के लिए माना जाता है जिसमें कम से कम 3 मान होते हैं, जिसका प्रकार टेम्पलेट पैरामीटर के रूप में दिया जाता है T। इसलिए नामित संदर्भों का उपयोग करने के बजाय जो Dataवर्ग के सदस्य हैं , यह एक्सेस के बिंदु पर संदर्भों को पॉप्युलेट करके मेमोरी को बचाता है (लेकिन दुर्भाग्य से, ऑपरेटर का उपयोग करके ->और नहीं .)।

यह जांचने के लिए कि कंपाइलर के ऑप्टिमाइज़र ने उपयोग के द्वारा शुरू किए गए सभी अप्रत्यक्ष को समाप्त कर दिया है Proxy, नीचे दिए गए कोड में 2% शामिल हैं main()#if 1संस्करण का उपयोग करता ->है और []ऑपरेटर, और #if 0संस्करण प्रदर्शन प्रक्रियाओं के बराबर सेट, लेकिन सीधे तक पहुँचने केवल द्वाराData::ar

Nci()समारोह सरणी तत्वों आरंभ, जो सिर्फ प्रत्येक में सीधे निरंतर मूल्यों को प्लग से अनुकूलक से बचाता है के लिए क्रम पूर्णांक मूल्यों उत्पन्न करता हैstd::cout << कॉल।

6.2 gcc के लिए, -O3 का उपयोग करते हुए, दोनों main()विधानसभा के दोनों संस्करण एक ही असेंबली उत्पन्न करते हैं ( तुलना करने वाले पहले #if 1और #if 0पहले टॉगल main()करें): https://godbolt.org/g/QqRWZb

#include <iostream>
#include <ctime>

template <typename T>
class Proxy {
public:
    T &a, &b, &c;
    Proxy(T* par) : a(par[0]), b(par[1]), c(par[2]) {}
    Proxy* operator -> () { return this; }
};

struct Data {
    int ar[3];
    template <typename I> int& operator [] (I idx) { return ar[idx]; }
    template <typename I> const int& operator [] (I idx) const { return ar[idx]; }
    Proxy<int>       operator -> ()       { return Proxy<int>(ar); }
    Proxy<const int> operator -> () const { return Proxy<const int>(ar); }
    int* begin()             { return ar; }
    const int* begin() const { return ar; }
    int* end()             { return ar + sizeof(ar)/sizeof(int); }
    const int* end() const { return ar + sizeof(ar)/sizeof(int); }
};

// Nci returns an unpredictible int
inline int Nci() {
    static auto t = std::time(nullptr) / 100 * 100;
    return static_cast<int>(t++ % 1000);
}

#if 1
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d->b << "\n";
    d->b = -5;
    std::cout << d[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd->c << "\n";
    //cd->c = -5;  // error: assignment of read-only location
    std::cout << cd[2] << "\n";
}
#else
int main() {
    Data d = {Nci(), Nci(), Nci()};
    for(auto v : d.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << d.ar[1] << "\n";
    d->b = -5;
    std::cout << d.ar[1] << "\n";
    std::cout << "\n";

    const Data cd = {Nci(), Nci(), Nci()};
    for(auto v : cd.ar) { std::cout << v << ' '; }
    std::cout << "\n";
    std::cout << cd.ar[2] << "\n";
    //cd.ar[2] = -5;
    std::cout << cd.ar[2] << "\n";
}
#endif

निफ्टी। मुख्य रूप से अपवित्र क्योंकि आपने साबित कर दिया कि यह दूर का अनुकूलन करता है। BTW, आप बहुत ही आसानी से एक बहुत ही सरल फ़ंक्शन लिखकर कर सकते हैं, पूरे main()समय के कार्यों के साथ नहीं ! उदाहरण के int getb(Data *d) { return (*d)->b; }लिए बस ( mov eax, DWORD PTR [rdi+4]/ godbolt.org/g/89d3Np ) संकलन करता है । (हाँ, वाक्यविन्यास को आसान बना देगा, लेकिन मैंने इस तरह से ओवरलोडिंग की अजीबता को उजागर करने के लिए रेफरी के बजाय एक सूचक का उपयोग किया ।)retData &d->
पीटर कॉर्ड्स

वैसे भी, यह अच्छा है। अन्य विचारों की तरह int tmp[] = { a, b, c}; return tmp[idx];दूर नहीं है, इसलिए यह साफ है कि यह करता है।
पीटर कॉर्ड्स

एक और कारण मैं operator.सी ++ 17 में याद करता हूं ।
जेन्स

2

यदि मान पढ़ना पर्याप्त है, और दक्षता चिंता का विषय नहीं है, या यदि आप चीजों को अच्छी तरह से अनुकूलित करने के लिए अपने संकलक पर भरोसा करते हैं, या यदि संरचना सिर्फ 3 बाइट्स है, तो आप सुरक्षित रूप से ऐसा कर सकते हैं:

char index_data(const struct data *d, size_t index) {
  assert(sizeof(*d) == offsetoff(*d, c)+1);
  assert(index < sizeof(*d));
  char buf[sizeof(*d)];
  memcpy(buf, d, sizeof(*d));
  return buf[index];
}

सी ++ केवल संस्करण के लिए, आप शायद static_assertयह सत्यापित करने के लिए उपयोग करना चाहेंगे कि struct dataमानक लेआउट है, और शायद इसके बजाय अमान्य सूचकांक पर अपवाद फेंक दें।


1

यह गैरकानूनी है, लेकिन एक समस्या है:

struct data {
    union {
        struct {
            int a;
            int b;
            int c;
        };
        int v[3];
    };
};

अब आप v को अनुक्रमित कर सकते हैं:


6
कई सी + + परियोजनाओं को लगता है कि सभी जगह डाउनकास्टिंग ठीक है। हमें अभी भी बुरी प्रथाओं का प्रचार नहीं करना चाहिए।
स्टोरीटेलर - अनसलैंडर मोनिका

2
संघ दोनों भाषाओं में सख्त अलियासिंग मुद्दे को हल करता है। लेकिन यूनियनों के माध्यम से टाइपिंग केवल सी में ठीक है, सी ++ में नहीं।
लुंडिन

1
फिर भी, मुझे आश्चर्य नहीं होगा अगर यह सभी c ++ संकलक के 100% पर काम करता है। कभी।
स्वेन निल्सन

1
आप इसे सबसे आक्रामक अनुकूलक सेटिंग के साथ gcc में आज़मा सकते हैं।
लुंडिन

1
@ लुंडिन: ISO C ++ के विस्तार के रूप में, GNU C ++ में यूनियन टाइप पिंगल लीगल है । यह मैनुअल में बहुत स्पष्ट रूप से कहा गया प्रतीत नहीं होता है , लेकिन मुझे इस पर पूरा यकीन है। फिर भी, इस उत्तर को यह समझाने की आवश्यकता है कि यह कहाँ मान्य है और कहाँ नहीं है।
पीटर कॉर्डेस
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.