वस्तु जीवनकाल के आक्रमणकारी बनाम शब्दार्थ को आगे बढ़ाते हैं


13

जब मैंने C ++ को बहुत पहले सीखा था, तो मुझे इस बात पर ज़ोर दिया गया था कि C ++ के बिंदु का हिस्सा यह है कि जैसे लूप में "लूप-इनवेरिएंट" होता है, कक्षाओं में भी ऑब्जेक्ट के जीवनकाल से जुड़े इन्वर्टर होते हैं - चीजें जो सच होनी चाहिए जब तक वस्तु जीवित है। ऐसी चीजें जिन्हें निर्माणकर्ताओं द्वारा स्थापित किया जाना चाहिए, और विधियों द्वारा संरक्षित किया जाना चाहिए। इनकैप्सुलेशन / ऐक्सेस कंट्रोल वहां मौजूद है जो आपको इनवियर्स लागू करने में मदद करता है। RAII एक चीज है जिसे आप इस विचार के साथ कर सकते हैं।

C ++ 11 के बाद से अब हमारे पास शब्दार्थ हैं। एक वर्ग के लिए जो गति का समर्थन करता है, किसी वस्तु से आगे बढ़ना औपचारिक रूप से अपने जीवन-समय को समाप्त नहीं करता है - यह कदम इसे कुछ "मान्य" स्थिति में छोड़ने वाला है।

एक कक्षा डिजाइन करने में, क्या यह बुरा अभ्यास है यदि आप इसे डिजाइन करते हैं ताकि वर्ग के आक्रमणकारियों को केवल उस बिंदु तक संरक्षित किया जाए जिससे इसे स्थानांतरित किया जाता है? या यह ठीक है कि अगर यह आपको तेजी से आगे बढ़ने की अनुमति देगा।

इसे ठोस बनाने के लिए, मान लें कि मेरे पास एक गैर-प्रतिलिपि योग्य लेकिन जंगम संसाधन प्रकार है जैसे:

class opaque {
  opaque(const opaque &) = delete;

public:
  opaque(opaque &&);

  ...

  void mysterious();
  void mysterious(int);
  void mysterious(std::vector<std::string>);
};

और जो भी कारण के लिए, मुझे इस ऑब्जेक्ट के लिए एक कॉपी करने योग्य आवरण बनाने की आवश्यकता है, ताकि इसका उपयोग किया जा सके, शायद कुछ मौजूदा प्रेषण प्रणाली में।

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { o_->mysterious(); }
  void operator()(int i) { o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};

इस copyable_opaqueऑब्जेक्ट में, निर्माण पर स्थापित वर्ग का एक अपरिवर्तनीय यह है कि सदस्य o_हमेशा एक वैध ऑब्जेक्ट को इंगित करता है, क्योंकि कोई डिफ़ॉल्ट ctor नहीं है, और केवल ctor जो प्रतिलिपि ctor नहीं है, इनकी गारंटी देता है। सभी operator()विधियां मानती हैं कि यह अपरिवर्तनीय धारण करता है, और बाद में इसे संरक्षित करता है।

हालाँकि, यदि ऑब्जेक्ट से ले जाया जाता है, तो o_फिर कुछ नहीं करने के लिए इंगित करेगा। और उस बिंदु के बाद, किसी भी तरीके operator()को कॉल करने से यूबी / क्रैश हो जाएगा।

यदि ऑब्जेक्ट को कभी भी वहां से नहीं ले जाया जाता है, तो आक्रमणकर्ता को सीधे डॉटर कॉल तक संरक्षित किया जाएगा।

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

विचार:

  1. यदि आप उन्हें छूते हैं तो ज़ोंबी ऑब्जेक्ट बनाने के लिए यह आमतौर पर C ++ में खराब रूप है।
    यदि आप किसी वस्तु का निर्माण नहीं कर सकते, तो आक्रमणकारियों को स्थापित नहीं कर सकते, तो ctor से एक अपवाद फेंक दें। यदि आप किसी विधि में आक्रमणकारियों को संरक्षित नहीं कर सकते हैं, तो किसी तरह त्रुटि का संकेत दें और रोल-बैक करें। क्या यह स्थानांतरित वस्तुओं से अलग होना चाहिए?

  2. क्या यह केवल "दस्तावेज़ के लिए पर्याप्त है" इस ऑब्जेक्ट को स्थानांतरित करने के बाद, हेडर में इसे नष्ट करने के अलावा इसके साथ कुछ भी करना अवैध (यूबी) है?

  3. क्या लगातार यह दावा करना बेहतर है कि यह प्रत्येक विधि कॉल में मान्य है?

इस तरह:

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { assert(o_); o_->mysterious(); }
  void operator()(int i) { assert(o_); o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};

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

  1. क्या कक्षा को नकल योग्य बनाना बेहतर है, लेकिन चल नहीं सकता?
    यह भी बुरा लगता है और एक प्रदर्शन हिट का कारण बनता है, लेकिन यह "अपरिवर्तनीय" मुद्दे को एक सरल तरीके से हल करता है।

आप यहां प्रासंगिक "सर्वोत्तम प्रथाओं" को क्या मानेंगे?



जवाबों:


20

यदि आप उन्हें छूते हैं तो ज़ोंबी ऑब्जेक्ट बनाने के लिए यह आमतौर पर C ++ में खराब रूप है।

लेकिन यह वह नहीं है जो आप कर रहे हैं। यदि आप इसे गलत तरीके से छूते हैं तो आप "ज़ोंबी ऑब्जेक्ट" बना रहे हैं । जो अंततः किसी भी अन्य राज्य-आधारित पूर्व शर्त से अलग नहीं है।

निम्नलिखित कार्य पर विचार करें:

void func(std::vector<int> &v)
{
  v[0] = 5;
}

क्या यह फ़ंक्शन सुरक्षित है? नहीं; उपयोगकर्ता एक खाली पास कर सकता है vector। इसलिए फ़ंक्शन में एक वास्तविक तथ्य है, vजिसमें कम से कम एक तत्व होता है। यदि ऐसा नहीं होता है, तो आप कॉल करते समय यूबी प्राप्त करते हैं func

इसलिए यह फ़ंक्शन "सुरक्षित" नहीं है। लेकिन इसका मतलब यह नहीं है कि यह टूट गया है। यह केवल तभी टूट जाता है जब इसका उपयोग करने वाला कोड पूर्व शर्त का उल्लंघन करता है। शायद funcअन्य कार्यों के कार्यान्वयन में सहायक के रूप में उपयोग किया जाने वाला एक स्थिर कार्य है। इस तरह से स्थानीयकृत, कोई भी इसे इस तरह से नहीं कहेगा जो इसके पूर्व शर्त का उल्लंघन करता हो।

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

C ++ मानक पुस्तकालय एक "वैध-लेकिन-अनिर्दिष्ट" नियम को परिभाषित करता है। यह कहता है कि, जब तक कि मानक अन्यथा नहीं कहता है, तब तक हर वस्तु जो स्थानांतरित की जाती है, वह मान्य होगी (यह उस प्रकार की कानूनी वस्तु है), लेकिन उस वस्तु की विशिष्ट स्थिति निर्दिष्ट नहीं है। एक स्थानांतरित-से कितने तत्व vectorहैं? यह नहीं कहता।

इसका मतलब यह है कि आप किसी भी फ़ंक्शन को नहीं कह सकते हैं जिसमें कोई पूर्व शर्त है। vector::operator[]पूर्व शर्त है कि vectorकम से कम एक तत्व है। चूंकि आपको इसकी स्थिति का पता नहीं है vector, इसलिए आप इसे कॉल नहीं कर सकते। यह funcपहले से सत्यापित किए बिना कॉल करने से बेहतर नहीं होगा कि vectorखाली न हो।

लेकिन इसका मतलब यह भी है कि जिन कार्यों में पूर्व शर्त नहीं हैं वे ठीक हैं। यह पूरी तरह से कानूनी C ++ 11 कोड है:

vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2{std::move(v1)};
v1.assign({6, 7, 8, 9, 10});

vector::assignकोई पूर्व शर्त नहीं है। यह किसी भी वैध vectorऑब्जेक्ट के साथ काम करेगा , यहां तक ​​कि एक जिसे से स्थानांतरित किया गया है।

इसलिए आप एक ऐसी वस्तु नहीं बना रहे हैं जो टूटी हुई हो। आप एक ऐसी वस्तु बना रहे हैं, जिसकी स्थिति अज्ञात है।

यदि आप किसी वस्तु का निर्माण नहीं कर सकते, तो आक्रमणकारियों को स्थापित नहीं कर सकते, तो ctor से एक अपवाद फेंक दें। यदि आप किसी विधि में आक्रमणकारियों को संरक्षित नहीं कर सकते हैं, तो किसी तरह त्रुटि का संकेत दें और रोल-बैक करें। क्या यह स्थानांतरित वस्तुओं से अलग होना चाहिए?

एक चाल निर्माणकर्ता से अपवादों को फेंकना आम तौर पर माना जाता है ... असभ्य। यदि आप एक ऐसी वस्तु को स्थानांतरित करते हैं जो स्मृति का स्वामी है, तो आप उस स्मृति के स्वामित्व को स्थानांतरित कर रहे हैं। और वह आमतौर पर कुछ भी शामिल नहीं करता है जो फेंक सकता है।

अफसोस की बात है कि हम इसे विभिन्न कारणों से लागू नहीं कर सकते । हमें स्वीकार करना होगा कि फेंक-चाल एक संभावना है।

यह भी ध्यान दिया जाना चाहिए कि आपको "मान्य-अभी तक अनिर्दिष्ट" भाषा का पालन नहीं करना है। ठीक उसी तरह जिस तरह C ++ मानक पुस्तकालय कहता है कि मानक प्रकारों के लिए आंदोलन डिफ़ॉल्ट रूप से काम करता है । कुछ मानक पुस्तकालय प्रकारों की अधिक सख्त गारंटी है। उदाहरण के लिए, unique_ptrएक स्थानांतरित- unique_ptrउदाहरण की स्थिति के बारे में बहुत स्पष्ट है : यह बराबर है nullptr

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

बस इतना ध्यान रखें: आंदोलन एक है प्रदर्शन के अनुकूलन , एक जो आम तौर पर वस्तुओं है कि कर रहे हैं पर किया जा रहा है के बारे में नष्ट होने के लिए। इस कोड पर विचार करें:

vector<int> func()
{
  vector<int> v;
  //fill up `v`.
  return v;
}

यह vरिटर्न वैल्यू से आगे बढ़ेगा (यह मानते हुए कि कंपाइलर इसे खत्म नहीं करता है)। और इस कदम के पूरा होने के बाद संदर्भ देने का कोई तरीका नहीं हैv । इसलिए आपने vएक उपयोगी स्थिति में लाने के लिए जो भी काम किया वह निरर्थक है।

अधिकांश कोड में, एक स्थानांतरित-से-ऑब्जेक्ट ऑब्जेक्ट का उपयोग करने की संभावना कम है।

क्या यह केवल "दस्तावेज़ के लिए पर्याप्त है" इस ऑब्जेक्ट को स्थानांतरित करने के बाद, हेडर में इसे नष्ट करने के अलावा इसके साथ कुछ भी करना अवैध (यूबी) है?

क्या लगातार यह दावा करना बेहतर है कि यह प्रत्येक विधि कॉल में मान्य है?

प्रीकॉन्डिशंस होने के पूरे बिंदु ऐसी चीजों की जांच नहीं करना हैoperator[]एक पूर्व शर्त है कि vectorदिए गए सूचकांक के साथ एक तत्व है। यदि आप के आकार के बाहर तक पहुँचने का प्रयास करते हैं तो आपको यूबी मिलता है vector। इस तरह के एक पूर्व शर्त vector::at नहीं है; यह स्पष्ट रूप से एक अपवाद फेंकता है यदि vectorऐसा मूल्य नहीं है।

प्रदर्शन कारणों के लिए पूर्व शर्त मौजूद हैं। वे ऐसा कर रहे हैं कि आपको उन चीज़ों की जाँच करने की ज़रूरत नहीं है जिन्हें कॉलर खुद के लिए सत्यापित कर सकता है। v[0]अगर vखाली है तो हर कॉल की जाँच नहीं करनी होगी ; केवल पहला ही करता है।

क्या कक्षा को नकल योग्य बनाना बेहतर है, लेकिन चल नहीं सकता?

नहीं, वास्तव में, एक वर्ग को कभी भी "कॉपी करने योग्य नहीं बल्कि चलने योग्य नहीं" होना चाहिए । यदि इसे कॉपी किया जा सकता है, तो इसे कॉपी कंस्ट्रक्टर को कॉल करके स्थानांतरित किया जा सकता है। यह C ++ 11 का मानक व्यवहार है यदि आप उपयोगकर्ता द्वारा परिभाषित कॉपी कंस्ट्रक्टर की घोषणा करते हैं, लेकिन एक मूव कंस्ट्रक्टर घोषित नहीं करते हैं। और यह वह व्यवहार है जिसे आपको अपनाना चाहिए यदि आप विशेष चाल शब्दार्थ को लागू नहीं करना चाहते हैं।

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


5
अच्छा लगा। +1। मैं यह नोट करूंगा कि: "पूर्वगामी होने का पूरा बिंदु ऐसी चीजों की जांच नहीं करना है।" - मुझे नहीं लगता कि यह दावा है। प्राथमिकताओं की जांच करने के लिए IMHO एक अच्छा और वैध उपकरण है (अधिकांश समय कम से कम।)
मार्टिन बा

3
प्रतिलिपि / चाल भ्रम को स्पष्ट करके यह समझा जा सकता है कि एक चाल ctor किसी भी राज्य में स्रोत वस्तु को छोड़ सकता है, जिसमें नई वस्तु के समान है - जिसका अर्थ है कि संभावित परिणाम प्रतिलिपि ctor के एक सुपरसेट हैं।
एमएसल्टर्स
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.