प्रतिनिधि इंटरव्यू विषय

C++ इंटरव्यू: आप std::expected के साथ एक कंपोज़ेबल एरर कॉन्ट्रैक्ट कैसे डिज़ाइन करेंगे?

कोडिंगमध्यम
Offer.cc संपादकीय टीमप्रकाशित अपडेट किया गया

प्रश्न

एक ऑर्डर सेवा वैलिडेशन, इन्वेंट्री रिज़र्वेशन और चार्जिंग के दौरान विफल हो सकती है। C++23 std::expected के साथ एरर-प्रोपेगेशन चेन डिज़ाइन करें और बताएं कि सीमा (boundary) पर अपवाद (exceptions) कब प्रासंगिक होते हैं।

प्रॉम्प्ट और संदर्भ

एक ऑर्डर सेवा इनपुट वैलिडेशन, इन्वेंट्री रिज़र्वेशन और चार्जिंग करती है। आउट-ऑफ-स्टॉक, अस्वीकृत भुगतान और डिपेंडेंसी टाइमआउट अपेक्षित विफलताएं (expected failures) हैं; प्रोग्रामिंग एरर, टूटे हुए इनवेरिएंट्स और गैर-रिकवरेबल रिसोर्स विफलताओं के लिए एक अलग पाथ की आवश्यकता होती है। साक्षात्कारकर्ता आपसे C++23 std::expected के साथ रिटर्न टाइप्स, कंपोज़िशन और एरर रिकॉर्डिंग डिज़ाइन करने के लिए कहता है।

यह एरर कॉन्ट्रैक्ट्स और कंपोज़ेबल कोड का परीक्षण करता है। std::expected<T, E> में या तो एक मान T होता है या एक एरर E; यह आपके लिए पुनः प्रयास (retry), लॉग या साइड इफेक्ट्स को रोलबैक नहीं करता है। पहले एरर डोमेन को परिभाषित करें, फिर दिखाएं कि कॉलर परिणामों का निरीक्षण कैसे करते हैं, और अंत में अपवाद और कंपंसेशन सीमाओं को बताएं।

साक्षात्कारकर्ता क्या मूल्यांकन कर रहा है

  • क्या आप व्यावसायिक विफलता (business failure), डिपेंडेंसी विफलता और टूटे हुए इनवेरिएंट्स के बीच अंतर करते हैं।
  • क्या आप has_value, value, error, unexpected, और [[nodiscard]] का सही उपयोग करते हैं।
  • क्या आप and_then, transform, और or_else के साथ नॉन-थ्रोइंग विफलताओं को कंपोज़ कर सकते हैं।
  • क्या आप std::expected को ग्लोबल एक्सेप्शन रिप्लेसमेंट या रॉ स्ट्रिंग एरर के रूप में मानने से बचते हैं।
  • क्या आप एरर मैपिंग, आइडेम्पोटेंसी, लॉगिंग और कंपंसेशन को कवर करते हैं।

पहले स्पष्ट करने योग्य प्रश्न

  • क्या कोडबेस C++23 के रूप में कंपाइल होता है, और क्या इसकी स्टैंडर्ड लाइब्रेरी लक्षित API को लागू करती है?
  • क्या एरर को स्थानीय रूप से संभाला जाता है या किसी सर्विस बाउंड्री के पार सीरियलाइज़ किया जाता है? क्रॉस-सर्विस एरर के लिए स्थिर कोड की आवश्यकता होती है।
  • क्या कोई फ़ंक्शन लॉक, फ़ाइल हैंडल या रिज़र्वेशन का स्वामित्व रखता है? विफलता लौटाने से पहले क्लीनअप और कंपंसेशन का उल्लेख करें।
  • क्या ऑर्डर के चरण आइडेम्पोटेंट हैं? चार्ज का पुनः प्रयास करना इन्वेंट्री को दो बार रिलीज़ करने से अलग है।
  • किन डायग्नोस्टिक फ़ील्ड्स को बनाए रखा जाना चाहिए? उपयोगकर्ता संदेशों और आंतरिक लॉग को मिश्रित नहीं किया जाना चाहिए।

30-सेकंड का उत्तर ढांचा

“मैं अपेक्षित व्यावसायिक विफलताओं को std::expected<T, OrderError> के रूप में मॉडल करूंगा ताकि कॉलर उन्हें स्पष्ट रूप से संभाल सकें। मैं परिणामों को [[nodiscard]] के रूप में चिह्नित करूंगा, and_then के साथ वैलिडेशन, रिज़र्वेशन और चार्जिंग को कंपोज़ करूंगा, और सुसंगत मैपिंग व मेट्रिक्स के लिए or_else का उपयोग करूंगा। अपवाद टूटे हुए इनवेरिएंट्स, इनिशियलाइज़ेशन विफलता, या ऐसी सीमा के लिए रहते हैं जो सुरक्षित रूप से रिकवर नहीं हो सकती। प्रत्येक साइड इफेक्ट के लिए एक आइडेम्पोटेंसी की (key), कंपंसेशन योजना और गैर-संवेदनशील लॉगिंग की आवश्यकता होती है।”

गहन उत्तर

एरर डोमेन परिभाषित करें

OrderError में ऐसी श्रेणियां होनी चाहिए जिन पर कॉलर कार्रवाई कर सके, जैसे कि invalidrequest, outofstock, paymentdeclined, और dependency_timeout, साथ ही नियंत्रित आंतरिक संदर्भ। उपयोगकर्ता संदेश (copy), स्टैक ट्रेस और वेंडर पेलोड को एक स्ट्रिंग में न रखें। प्रोग्रामिंग एरर और टूटे हुए इनवेरिएंट्स को चुपचाप सामान्य व्यावसायिक विफलताओं में नहीं बदलना चाहिए।

मान और एरर प्रकार चुनें

वैलिडेशन std::expected<ValidatedOrder, OrderError> लौटा सकता है, रिज़र्वेशन std::expected<Reservation, OrderError> लौटा सकता है, और चार्जिंग std::expected<Receipt, OrderError> लौटा सकती है। एरर टाइप को मूवेबल, उचित रूप से छोटा और स्वामित्व के बारे में स्पष्ट रखें। परिणाम देने वाले फ़ंक्शनों को [[nodiscard]] के साथ चिह्नित करें ताकि कॉलर चुपचाप किसी विफलता को अनदेखा न कर सके।

स्पष्ट सीमाओं पर स्पष्ट जांच का उपयोग करें

जब चरण छोटे हों या प्रति एरर अलग-अलग क्रियाएं हों, तो स्पष्ट जांच का ऑडिट करना आसान होता है:

cpp
[[nodiscard]] auto reserve(const ValidatedOrder& order)
    -> std::expected<Reservation, OrderError>;

auto create_order(Input input) -> std::expected<Receipt, OrderError> {
  auto valid = validate(std::move(input));
  if (!valid) return std::unexpected(valid.error());

  auto held = reserve(*valid);
  if (!held) return std::unexpected(held.error());

  return charge(*valid, *held);
}

सफलता स्थापित करने के बाद ही operator* और value() का उपयोग करें। विफलता पाथ पर मूल श्रेणी को बनाए रखें; आउट-ऑफ-स्टॉक को एक अस्पष्ट जेनेरिक एरर के रूप में न छिपाएं।

मोनाडिक ऑपरेशंस के साथ होमोजेनियस चेन्स कंपोज़ करें

जब प्रत्येक चरण std::expected लौटाता है, तो and_then केवल एक मान पर जारी रहता है और एरर होने पर शॉर्ट-सर्किट करता है। transform एक सफल मान को मैप करता है, or_else एक एरर को रिकॉर्ड या कन्वर्ट करता है, और C++23 बाउंड्री कन्वर्जन के लिए transform_error भी प्रदान करता है। साइड-इफेक्ट क्रम को दृश्यमान रखें; एक गैर-ऑडिट योग्य श्रृंखला के अंदर चार्जिंग, पुनः प्रयास और कंपंसेशन को न छिपाएं।

एक्सेप्शन सीमा निर्धारित करें

टाइमआउट, आउट-ऑफ-स्टॉक और अस्वीकृत भुगतान अपेक्षित परिणाम हैं, इसलिए expected व्यावसायिक परत को पुनः प्रयास, उपयोगकर्ता प्रतिक्रिया, या मैन्युअल हैंडलिंग चुनने की अनुमति देता है। टूटे हुए इनवेरिएंट्स, इनिशियलाइज़ेशन विफलता, या ऐसी सीमा जो सुरक्षित रूप से रिकवर नहीं हो सकती, अपवादों का उपयोग कर सकती है। सीमा को सुसंगत रखें: एक ही एरर श्रेणी को एक ही फ़ंक्शन से कभी वापस नहीं लौटना चाहिए और कभी थ्रो नहीं करना चाहिए।

साइड इफेक्ट्स, आइडेम्पोटेंसी और कंपंसेशन संभालें

expected एक परिणाम ट्रांसपोर्ट करता है; यह पूर्ण किए गए साइड इफेक्ट को पूर्ववत (undo) नहीं करता है। यदि रिज़र्वेशन सफल होता है और चार्जिंग विफल हो जाती है, तो रिज़र्वेशन को रिलीज़ करें या कंपंसेटिंग स्थिति में प्रवेश करें। चार्ज पुनः प्रयासों के लिए एक आइडेम्पोटेंसी की की आवश्यकता होती है। एरर ऑब्जेक्ट्स बिना किसी गोपनीय जानकारी (secrets) के ऑर्डर आईडी, चरण और पुनः प्रयास सलाह ले जा सकते हैं; लॉगिंग भुगतान क्रेडेंशियल्स को संग्रहीत किए बिना एक ट्रेस को संबद्ध कर सकती है।

अनुबंध को परीक्षण योग्य बनाएं

प्रत्येक एरर ब्रांच, शॉर्ट-सर्किट, मैपिंग और कंपंसेशन क्रम का परीक्षण करें। परीक्षण करें कि अप्रत्याशित अपवाद सीमा पार करते हैं और सुसंगत रूप से संभाले जाते हैं। CI में कंपाइलर, स्टैंडर्ड-लाइब्रेरी फीचर मैक्रोज़ और बिल्ड फ़्लैग्स को पिन करें। सेवाओं के पार, API अनुबंधों के रूप में C++ टाइप नामों को उजागर करने के बजाय OrderError को स्थिर प्रोटोकॉल कोड पर मैप करें।

मॉडल उच्च-गुणवत्ता वाला उत्तर

“मैं स्थिर श्रेणियों और नियंत्रित आंतरिक डायग्नोस्टिक्स के साथ OrderError को परिभाषित करूंगा। वैलिडेशन, रिज़र्वेशन और चार्जिंग [[nodiscard]] std::expected मान लौटाते हैं। आउट-ऑफ-स्टॉक, अस्वीकृत भुगतान और टाइमआउट व्यावसायिक विफलताएं हैं जिन्हें कॉलर द्वारा स्पष्ट रूप से संभाला जाता है; वे सभी अपवाद नहीं हैं।

एक छोटी श्रृंखला के लिए मैं if (!result) का उपयोग करूंगा और std::unexpected(result.error()) को प्रोपेगेट करूंगा, जिससे प्रत्येक साइड-इफेक्ट सीमा ऑडिट योग्य रहे। शुद्ध होमोजेनियस ट्रांसफॉर्मेशन and_then और transform का उपयोग कर सकते हैं, जबकि or_else एरर को रिकॉर्ड और मैप करता है। मैं सफलता स्थापित होने के बाद ही value() को कॉल करता हूं।

यदि रिज़र्वेशन सफल होता है और चार्जिंग विफल हो जाती है, तो expected इसे रोलबैक नहीं करता है, इसलिए मैं एक आइडेम्पोटेंट स्थिति रिकॉर्ड करता हूं और रिलीज़ या कंपंसेशन चलाता हूं। अपवाद टूटे हुए इनवेरिएंट्स और इनिशियलाइज़ेशन विफलताओं के लिए आरक्षित हैं जिन्हें वर्तमान सीमा सुरक्षित रूप से रिकवर नहीं कर सकती है। CI C++23 टूलचेन को पिन करता है और एरर को स्थिर सर्विस कोड पर मैप करने से पहले प्रत्येक एरर, शॉर्ट-सर्किट और कंपंसेशन पाथ का परीक्षण करता है।”

सामान्य गलतियाँ

  • std::expected को पुनः प्रयास इंजन के रूप में मानना: रिटर्न प्रकार में कोई पुनः प्रयास सिमेंटिक्स नहीं होते हैं → एरर श्रेणियों और आइडेम्पोटेंसी का उपयोग करके व्यावसायिक परत में निर्णय लें।
  • std::optional का उपयोग करना और कारण खो देना: कॉलर स्टॉक विफलता को टाइमआउट से अलग नहीं कर सकते → एक स्थिर एरर प्रकार का उपयोग करें।
  • [[nodiscard]] को अनदेखा करना: कॉलर किसी विफलता को छोड़ सकते हैं → परिणाम फ़ंक्शनों को एनोटेट करें और CI में चेतावनियों को बढ़ावा दें।
  • जांच किए बिना value() को कॉल करना: एक विफलता bad_expected_access थ्रो कर सकती है → स्पष्ट रूप से ब्रांच करें या पहले जांचें।
  • प्रत्येक अपवाद को एक एरर कोड में बदलना: टूटे हुए इनवेरिएंट्स दब सकते हैं → एक स्पष्ट अपवाद सीमा बनाए रखें।
  • एरर ऑब्जेक्ट में गोपनीय जानकारी (secrets) डालना: लॉग या सीरियलाइज़ेशन क्रेडेंशियल्स लीक कर सकते हैं → उपयोगकर्ता संदेश, स्थिर कोड और आंतरिक डायग्नोस्टिक्स को अलग करें।
  • पूर्ण किए गए साइड इफेक्ट्स की अनदेखी करना: विफल चार्ज के बाद इन्वेंट्री आरक्षित रहती है → आइडेम्पोटेंट स्थिति और कंपंसेशन डिज़ाइन करें।
  • सेवाओं के पार C++ प्रकार भेजना: अपग्रेड प्रोटोकॉल को तोड़ते हैं → संस्करणित स्थिर कोड और फ़ील्ड्स पर मैप करें।

फॉलो-अप प्रश्न और उत्तर

फॉलो-अप 1: आप std::expected और अपवादों के बीच कैसे चयन करते हैं?

अपेक्षित व्यावसायिक परिणामों के लिए expected का उपयोग करें जिन्हें कॉलर संभाल सकता है। टूटे हुए इनवेरिएंट्स, इनिशियलाइज़ेशन विफलता, या ऐसी सीमा के लिए अपवादों का उपयोग करें जो सुरक्षित रूप से रिकवर नहीं हो सकती। एक सार्वभौमिक नियम से अधिक संगति मायने रखती है: कॉलर्स को एक एरर श्रेणी के लिए नियंत्रण प्रवाह का अनुमान नहीं लगाना चाहिए।

फॉलो-अप 2: std::optional<T> क्यों नहीं?

optional उपस्थिति या अनुपस्थिति का प्रतिनिधित्व करता है लेकिन कोई कारण नहीं बताता है। ऑर्डर प्रवाह को इन्वेंट्री, भुगतान और डिपेंडेंसी विफलताओं के लिए अलग-अलग कार्यों की आवश्यकता होती है, इसलिए इसे expected<T, E> की आवश्यकता होती है।

फॉलो-अप 3: क्या चार्जिंग and_then के अंदर चल सकती है?

यह चल सकती है, यदि साइड-इफेक्ट सीमाएं दृश्यमान रहें और प्रत्येक चरण पुनः प्रयास करने योग्य या कंपंसेट करने योग्य हो। विभिन्न नीतियों वाली एक लंबी श्रृंखला के लिए, स्पष्ट if का ऑडिट करना आसान हो सकता है; केवल एक कार्यात्मक शैली (functional-looking style) के लिए स्थिति परिवर्तनों को न छिपाएं।

फॉलो-अप 4: आप कई डिपेंडेंसीज़ से आने वाली एरर को कैसे एकीकृत करते हैं?

सर्विस बाउंड्री पर एक स्थिर डोमेन एरर परिभाषित करें। आंतरिक कारण को बनाए रखते हुए वेंडर की विफलताओं को श्रेणियों के एक सीमित सेट में मैप करें। मैपिंग के लिए transform_error या or_else का उपयोग करें, वेंडर कोड और ट्रेस को आंतरिक रूप से रिकॉर्ड करें, और वेंडर की शब्दावली को उपयोगकर्ता संदेशों से बाहर रखें।

फॉलो-अप 5: क्या एरर लौटाना अपवाद थ्रो करने से धीमा है?

वास्तविक पाथ्स को मापें। expected सामान्य विफलताओं को स्पष्ट बनाता है लेकिन किसी एरर ऑब्जेक्ट को कॉपी कर सकता है या शाखाएं जोड़ सकता है; अपवाद थ्रो पाथ पर लागत केंद्रित करते हैं। किसी पूर्ण दावे के बजाय लेटेंसी लक्ष्यों, कंपाइलर व्यवहार और एरर-फ़्रीक्वेंसी बेंचमार्क से चयन करें।

फॉलो-अप 6: आप कॉलर्स को जांचना भूलने से कैसे रोकते हैं?

परिणाम प्रकारों और महत्वपूर्ण फ़ंक्शनों को [[nodiscard]] के साथ एनोटेट करें, कंपाइलर चेतावनियों को CI विफलताओं में बदलें, और प्रत्येक value() कॉल की समीक्षा करें। क्रॉस-लैंग्वेज API के लिए, प्रोटोकॉल-स्टेट और कॉन्ट्रैक्ट टेस्ट जोड़ें ताकि वह कवर किया जा सके जिसे कंपाइलर लागू नहीं कर सकता।

सार्वजनिक स्रोत

संबंधित प्रश्न

संबंधित इंटरव्यू टूल

कोडिंग प्रॉम्प्ट के लिए स्क्रीनशॉट का उपयोग करें

समस्या को कैप्चर करें, फिर क्रम से प्रतिबंधों (constraints), समाधान, कोड, एज केस और जटिलता पर काम करें।

टूल देखें