फ़ंक्शन अनजाने में संदर्भ पैरामीटर को अमान्य कर देता है - क्या गलत हुआ?


54

आज हमें एक बुरा बग का कारण पता चला जो केवल कुछ प्लेटफार्मों पर रुक-रुक कर हुआ। उबला हुआ, हमारे कोड इस तरह देखा:

class Foo {
  map<string,string> m;

  void A(const string& key) {
    m.erase(key);
    cout << "Erased: " << key; // oops
  }

  void B() {
    while (!m.empty()) {
      auto toDelete = m.begin();
      A(toDelete->first);
    }
  }
}

इस सरलीकृत मामले में समस्या स्पष्ट लग सकती है: Bकुंजी का संदर्भ Aदेता है, जो इसे प्रिंट करने का प्रयास करने से पहले मानचित्र प्रविष्टि को हटा देता है। (हमारे मामले में, यह मुद्रित नहीं किया गया था, लेकिन अधिक जटिल तरीके से उपयोग किया गया) यह निश्चित रूप से अपरिभाषित व्यवहार है, क्योंकि keyकॉल के बाद एक झूलने वाला संदर्भ है erase

इस फिक्सिंग तुच्छ था - हम सिर्फ से पैरामीटर प्रकार बदल const string&करने के लिए string। सवाल यह है कि हम पहली बार में इस बग से कैसे बच सकते थे? ऐसा लगता है कि दोनों कार्यों ने सही काम किया:

  • Aयह जानने का कोई तरीका नहीं है कि यह उस keyचीज़ को संदर्भित करता है जो इसे नष्ट करने वाली है।
  • Bइसे पास करने से पहले एक प्रति बना सकता था A, लेकिन क्या यह मानने या संदर्भ द्वारा मानदंड लेने का फैसला करना कैली का काम नहीं है?

क्या कोई नियम है जिसका हम पालन करने में विफल रहे हैं?

जवाबों:


35

Aयह जानने का कोई तरीका नहीं है कि यह उस keyचीज़ को संदर्भित करता है जो इसे नष्ट करने वाली है।

हालांकि यह सच है, Aनिम्न बातें जानते हैं:

  1. इसका उद्देश्य कुछ नष्ट करना है

  2. यह एक पैरामीटर लेता है जो उसी प्रकार की चीज़ का होता है जिसे वह नष्ट कर देगा।

इन तथ्यों को देखते हुए यह है संभव के लिए Aअपने स्वयं के पैरामीटर को नष्ट करने के अगर यह एक सूचक / संदर्भ के रूप में पैरामीटर लेता है। यह सी ++ में एकमात्र जगह नहीं है जहां इस तरह के विचारों को संबोधित करने की आवश्यकता है।

यह स्थिति समान है कि operator=असाइनमेंट ऑपरेटर की प्रकृति का अर्थ है कि आपको स्वयं असाइनमेंट के बारे में चिंतित होने की आवश्यकता हो सकती है। यह एक संभावना है क्योंकि thisसंदर्भ पैरामीटर के प्रकार और प्रकार समान हैं।

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

यह एक टिप्पणी के लिए एक अच्छी जगह होगी।

क्या कोई नियम है जिसका हम पालन करने में विफल रहे हैं?

C ++ में, आप इस धारणा के तहत काम नहीं कर सकते हैं कि यदि आप नेत्रहीन रूप से नियमों के एक सेट का पालन करते हैं, तो आपका कोड 100% सुरक्षित होगा। हमारे पास हर चीज के लिए नियम नहीं हो सकते ।

ऊपर बिंदु # 2 पर विचार करें। Aकुंजी से भिन्न प्रकार के कुछ पैरामीटर ले सकता था, लेकिन ऑब्जेक्ट स्वयं मानचित्र में एक कुंजी का एक उप-विषय हो सकता है। C ++ 14 में, findकुंजी प्रकार से भिन्न प्रकार ले सकते हैं, जब तक कि उनके बीच एक वैध तुलना हो। इसलिए यदि आप करते हैं m.erase(m.find(key)), तो आप पैरामीटर को नष्ट कर सकते हैं , भले ही पैरामर का प्रकार प्रमुख प्रकार न हो।

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

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


10
ठीक है, आपके पास नियम हो सकता है "कभी भी साझा उत्परिवर्तित स्थिति" या यह दोहरी "कभी भी साझा की गई अवस्था नहीं है", लेकिन फिर आप पहचान योग्य सी + + लिखने के लिए संघर्ष करेंगे
Caleth

7
@ कैलेथ यदि आप उन नियमों का उपयोग करना चाहते हैं तो C ++ आपके लिए भाषा नहीं है।
user253751

3
@ कैलाथ आप जंग का वर्णन कर रहे हैं?
मैल्कम

1
"हमारे पास हर चीज के लिए नियम नहीं हो सकते।" हाँ हम कर सकते हैं। cstheory.stackexchange.com/q/4052
Ouroborus

23

मैं कहूंगा कि हां, एक बहुत ही सरल नियम है जिसे आपने तोड़ दिया है जो आपको बचा लेगा: एकल जिम्मेदारी सिद्धांत।

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

अगर हम एक समारोह है कि कि सिर्फ नक्शे से मूल्य को हटा देता है, और कहा कि एक और बस के नक्शे से एक मूल्य के प्रसंस्करण करता है, हम उच्च स्तर कोड से प्रत्येक कॉल करने के लिए होगा, इसलिए हम कुछ इस तरह को रखना होगा :

std::string &key = get_value_from_map();
destroy(key);
continue_to_use(key);

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


3
खैर यह एक मान्य अवलोकन है, यह इस मामले में केवल बहुत ही संकीर्ण रूप से लागू होता है। ऐसे बहुत सारे उदाहरण हैं जहां SRP का सम्मान किया जाता है और अभी भी फ़ंक्शन के मुद्दे संभावित रूप से अपने स्वयं के पैरामीटर को अमान्य कर रहे हैं।
बेन Voigt

5
@BenVoigt: बस अपने पैरामीटर को अमान्य करने से समस्या नहीं होती है। समस्याओं के लिए अमान्य होने के बाद यह पैरामीटर का उपयोग करना जारी रखता है। लेकिन अंत में हां, आप सही हैं: जबकि इसने उसे इस मामले में बचाया होगा, निस्संदेह ऐसे मामले हैं जहां यह अपर्याप्त है।
जेरी कॉफिन

3
एक सरलीकृत उदाहरण लिखते समय, आपको कुछ विवरणों को छोड़ना पड़ता है, और कभी-कभी यह पता चलता है कि उन विवरणों में से एक महत्वपूर्ण था। हमारे मामले में, Aवास्तव keyमें दो अलग-अलग मानचित्रों में देखा गया और, यदि पाया गया, तो प्रविष्टियों को हटा दिया गया और साथ ही कुछ अतिरिक्त सफाई भी। इसलिए यह स्पष्ट नहीं है कि हमारे Aउल्लंघन एसआरपी। मुझे आश्चर्य है कि अगर मुझे इस बिंदु पर सवाल अपडेट करना चाहिए।
निकोलाई

2
निकोलाई के उदाहरण में @BenVoigt के बिंदु पर विस्तार करने के लिए, m.erase(key)पहली जिम्मेदारी है, और cout << "Erased: " << keyदूसरी जिम्मेदारी है, इसलिए इस उत्तर में दिखाए गए कोड की संरचना वास्तव में उदाहरण में कोड की संरचना से अलग नहीं है, अभी तक वास्तविक दुनिया की समस्या को नजरअंदाज कर दिया गया था। एकल जिम्मेदारी सिद्धांत यह सुनिश्चित करने के लिए कुछ भी नहीं करता है, या यहां तक ​​कि इसे अधिक संभावना बनाता है, कि एकल कार्यों के विरोधाभासी अनुक्रम वास्तविक-दुनिया कोड में निकटता में दिखाई देंगे।
श्रीधाम

10

क्या कोई नियम है जिसका हम पालन करने में विफल रहे हैं?

हां, आप फ़ंक्शन को दस्तावेज़ करने में विफल रहे

पैरामीटर-गुजर अनुबंध के विवरण के बिना (विशेष रूप से पैरामीटर की वैधता से संबंधित भाग - क्या यह फ़ंक्शन कॉल की शुरुआत में या भर में है) यह बताना असंभव है कि क्या कार्यान्वयन में त्रुटि है (यदि कॉल अनुबंध है) क्या यह है कि कॉल शुरू होने पर पैरामीटर वैध है, फ़ंक्शन को पैरामीटर को अमान्य करने वाले किसी भी कार्य को करने से पहले एक प्रतिलिपि बनाना चाहिए) या कॉलर में (यदि कॉल अनुबंध यह है कि पैरामीटर को कॉल के दौरान वैध रहना चाहिए, तो कॉलर नहीं कर सकता संग्रह के अंदर डेटा के संदर्भ को संशोधित करें)।

उदाहरण के लिए, C ++ मानक ही निर्दिष्ट करता है कि:

यदि किसी फ़ंक्शन के तर्क में एक अमान्य मान है (जैसे फ़ंक्शन के डोमेन के बाहर एक मान या इसके इच्छित उपयोग के लिए कोई अमान्य अमान्य), तो व्यवहार अपरिभाषित है।

लेकिन यह निर्दिष्ट करने में विफल रहता है कि क्या यह केवल कॉल किए जाने के तुरंत बाद, या फ़ंक्शन के निष्पादन के दौरान लागू होता है। हालाँकि, कई मामलों में यह स्पष्ट है कि केवल उत्तरार्द्ध ही संभव है - अर्थात् जब तर्क को प्रतिलिपि बनाकर वैध नहीं रखा जा सकता है।

वास्तविक दुनिया के कुछ मामले ऐसे हैं, जहां यह अंतर खेल में आता है। उदाहरण के लिए, खुद को जोड़नाstd::vector<T>


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

@snowman दिलचस्प होते हुए, यूबी रिडरिंग पूरी तरह से असंबंधित है जो मैं इस उत्तर में चर्चा करता हूं, जो कि वैधता सुनिश्चित करने की जिम्मेदारी है (ताकि यूबी कभी भी न हो)।
बेन वोइगट

जो वास्तव में मेरी बात है: कोड लिखने वाले व्यक्ति को समस्याओं से भरे पूरे खरगोश छेद से बचने के लिए यूबी से बचने के लिए जिम्मेदार होने की आवश्यकता है।

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

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

2

क्या कोई नियम है जिसका हम पालन करने में विफल रहे हैं?

हां, आप इसे सही तरीके से परखने में असफल रहे। आप अकेले नहीं हैं, और आप सीखने के लिए सही जगह पर हैं :)


C ++ में बहुत सारे अनफाइंड बिहेवियर हैं, अनफाइंड बिहेवियर सूक्ष्म और कष्टप्रद तरीकों से प्रकट होता है।

आप शायद कभी भी 100% सुरक्षित C ++ कोड नहीं लिख सकते हैं, लेकिन आप निश्चित रूप से कई टूल का उपयोग करके अपने कोड बेस में अनजाने व्यवहार को शुरू करने की संभावना को कम कर सकते हैं।

  1. संकलक चेतावनी
  2. स्टेटिक विश्लेषण (चेतावनियों का विस्तारित संस्करण)
  3. इंस्ट्रूमेंट टेस्ट बायनेरिज़
  4. कठोर उत्पादन बायनेरिज़

आपके मामले में, मुझे संदेह है (1) और (2) ने बहुत मदद की होगी, हालांकि सामान्य तौर पर मैं उन्हें इस्तेमाल करने की सलाह देता हूं। अभी के लिए अन्य दो पर ध्यान केंद्रित करते हैं।

Gcc और Clang दोनों में एक -fsanitizeझंडा होता है जो विभिन्न प्रकार के मुद्दों की जांच करने के लिए आपके द्वारा संकलित किए गए कार्यक्रमों को लिखता है। -fsanitize=undefinedउदाहरण के लिए हस्ताक्षर किए गए पूर्णांक अंडरफ़्लो / अतिप्रवाह को पकड़ेंगे, बहुत अधिक मात्रा में स्थानांतरण, आदि ... आपके विशिष्ट मामले में, -fsanitize=addressऔर -fsanitize=memoryइस मुद्दे पर लेने की संभावना होगी ... बशर्ते आपके पास फ़ंक्शन को कॉल करने वाला एक परीक्षण हो। पूर्णता के लिए, -fsanitize=threadयदि आप एक बहु-थ्रेडेड कोडबेस का उपयोग करने के लायक है। यदि आप बाइनरी को लागू नहीं कर सकते हैं (उदाहरण के लिए, आपके पास उनके स्रोत के बिना 3 पार्टी लाइब्रेरी हैं), तो आप इसका उपयोग भी कर सकते हैं valgrindहालांकि यह सामान्य रूप से धीमा है।

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

दोनों (3) और (4) का बिंदु एक रुक-रुक कर विफलता को एक निश्चित विफलता में बदलना है : वे दोनों असफल तेजी सिद्धांत का पालन करते हैं । इस का मतलब है कि:

  • जब आप बारूदी सुरंग पर कदम रखते हैं तो यह हमेशा विफल रहता है
  • यह तुरंत विफल हो जाता है , आपको बेतरतीब ढंग से मेमोरी को दूषित करने आदि के बजाय त्रुटि पर इंगित करता है ...

एक अच्छा परीक्षण कवरेज के साथ संयोजन (3) उत्पादन से पहले अधिकांश मुद्दों को पकड़ना चाहिए। उत्पादन में (4) का उपयोग करना कष्टप्रद बग और शोषण के बीच अंतर हो सकता है।


0

@ नोट: यह पोस्ट बेन वोइगट के उत्तर के शीर्ष पर और अधिक तर्क जोड़ता है ।

सवाल यह है कि हम पहली बार में इस बग से कैसे बच सकते थे? ऐसा लगता है कि दोनों कार्यों ने सही काम किया:

  • A का यह जानने का कोई तरीका नहीं है कि कुंजी उस चीज़ को संदर्भित करती है जिसे वह नष्ट करने वाला है।
  • B इसे A से पास करने से पहले एक प्रति बना सकता था, लेकिन क्या यह मानने या संदर्भ द्वारा पैरामीटर लेने का निर्णय लेने के लिए कैली का काम नहीं है?

दोनों कार्यों ने सही काम किया।

समस्या क्लाइंट कोड के भीतर है, जिसने कॉलिंग ए के दुष्प्रभावों को ध्यान में नहीं रखा ।

C ++ के पास भाषा में साइड इफेक्ट्स निर्दिष्ट करने का कोई सीधा तरीका नहीं है।

इसका मतलब यह है कि यह आपके (और आपकी टीम) के ऊपर है कि यह सुनिश्चित करें कि साइड इफेक्ट्स जैसी चीजें कोड में (डॉक्यूमेंटेशन के रूप में) दिखाई दे रही हैं, और कोड के साथ बनाए रखी गई हैं (आपको शायद पूर्व-शर्तों, पोस्ट-कंडीशन और इनवॉयरमेंट का दस्तावेजीकरण करने पर विचार करना चाहिए) साथ ही, दृश्यता कारणों के लिए भी)।

कोड परिवर्तन:

class Foo {
  map<string,string> m;

  /// \sideeffect invalidates iterators
  void A(const string& key) {
    m.erase(key);
    cout << "Erased: " << key; // oops
  }
  ...

इस बिंदु पर आपके पास एपीआई के ऊपर कुछ है जो आपको बताता है कि आपके पास इसके लिए एक इकाई परीक्षण होना चाहिए; यह आपको एपीआई का उपयोग करने (और उपयोग न करने) का तरीका भी बताता है।


-4

हम पहली बार में इस बग से कैसे बच सकते थे?

बग से बचने का केवल एक ही तरीका है: कोड लिखना बंद करें। बाकी सब कुछ किसी तरह से विफल रहा।

हालांकि, विभिन्न स्तरों पर परीक्षण कोड (इकाई परीक्षण, कार्यात्मक परीक्षण, एकीकरण परीक्षण, स्वीकृति परीक्षण आदि) न केवल कोड की गुणवत्ता में सुधार करेंगे, बल्कि कीड़े की संख्या को भी कम करेंगे।


1
यह पूरी बकवास है। है सिर्फ एक ही रास्ता कीड़े से बचने के लिए। हालांकि यह पूरी तरह से सच है कि बग के अस्तित्व से पूरी तरह से बचने का एकमात्र तरीका कोड लिखना कभी नहीं है, यह भी सच है (और अधिक उपयोगी) कि विभिन्न सॉफ्टवेयर इंजीनियरिंग प्रक्रियाएं हैं जिनका आप पालन कर सकते हैं, दोनों जब शुरू में कोड लिखते हैं और इसका परीक्षण करते समय, यह कीड़े की उपस्थिति को काफी कम कर सकता है । परीक्षण चरण के बारे में हर कोई जानता है, लेकिन सबसे बड़ी प्रभाव अक्सर सबसे पहले लागत पर हो सकता है, पहली बार कोड लिखते समय जिम्मेदार डिजाइन प्रथाओं और मुहावरों का पालन करके।
कोड़ी ग्रे
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.