c++ सी++ में एक अपरिवर्तनीय और कुशल वर्ग बनाने के लिए मुहावरेदार तरीका




const immutability (8)

मैं ऐसा कुछ करना चाह रहा हूं (C #)।

public final class ImmutableClass {
    public readonly int i;
    public readonly OtherImmutableClass o;
    public readonly ReadOnlyCollection<OtherImmutableClass> r;

    public ImmutableClass(int i, OtherImmutableClass o,
        ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}

संभावित समाधान और उनसे जुड़ी समस्याओं का सामना कर रहे हैं:

1. वर्ग के सदस्यों के लिए const का उपयोग करना , लेकिन इसका मतलब है कि डिफ़ॉल्ट कॉपी असाइनमेंट ऑपरेटर हटा दिया गया है।

समाधान 1:

struct OtherImmutableObject {
    const int i1;
    const int i2;

    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
}

समस्या 1:

OtherImmutableObject o1(1,2);
OtherImmutableObject o2(2,3);
o1 = o2; // error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)`

संपादित करें: यह महत्वपूर्ण है क्योंकि मैं एक std::vector में अपरिवर्तनीय वस्तुओं को संग्रहीत करना चाहता हूं std::vector लेकिन error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(OtherImmutableObject&&) प्राप्त error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(OtherImmutableObject&&)

2. तरीकों और रिटर्न वैल्यू का उपयोग करना , लेकिन इसका मतलब है कि बड़ी वस्तुओं को कॉपी करना होगा जो एक अक्षमता है जो मैं जानना चाहता हूं कि कैसे बचें। यह थ्रेड समाधान प्राप्त करने का सुझाव देता है, लेकिन यह पता नहीं चलता है कि मूल वस्तु की नकल के बिना गैर-आदिम वस्तुओं को पारित करने के लिए कैसे संभालना है।

समाधान 2:

class OtherImmutableObject {
    int i1;
    int i2;
public:
    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    int GetI1() { return i1; }
    int GetI2() { return i2; }
}

class ImmutableObject {
    int i1;
    OtherImmutableObject o;
    std::vector<OtherImmutableObject> v;
public:
    ImmutableObject(int i1, OtherImmutableObject o,
        std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
    int GetI1() { return i1; }
    OtherImmutableObject GetO() { return o; } // Copies a value that should be immutable and therefore able to be safely used elsewhere.
    std::vector<OtherImmutableObject> GetV() { return v; } // Copies the vector.
}

समस्या 2: अनावश्यक प्रतियां अक्षम हैं।

3. प्राप्त तरीकों का उपयोग करना और const संदर्भ या const पॉइंटर्स को वापस करना लेकिन यह हैंगिंग रेफरेंस या पॉइंटर्स को छोड़ सकता है। यह थ्रेड फ़ंक्शन रिटर्न से दायरे से बाहर जाने वाले संदर्भों के खतरों के बारे में बात करता है।

समाधान 3:

class OtherImmutableObject {
    int i1;
    int i2;
public:
    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    int GetI1() { return i1; }
    int GetI2() { return i2; }
}

class ImmutableObject {
    int i1;
    OtherImmutableObject o;
    std::vector<OtherImmutableObject> v;
public:
    ImmutableObject(int i1, OtherImmutableObject o,
        std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
    int GetI1() { return i1; }
    const OtherImmutableObject& GetO() { return o; }
    const std::vector<OtherImmutableObject>& GetV() { return v; }
}

समस्या 3:

ImmutableObject immutable_object(1,o,v);
// elsewhere in code...
OtherImmutableObject& other_immutable_object = immutable_object.GetO();
// Somewhere else immutable_object goes out of scope, but not other_immutable_object
// ...and then...
other_immutable_object.GetI1();
// The previous line is undefined behaviour as immutable_object.o will have been deleted with immutable_object going out of scope

किसी भी तरीके से संदर्भ वापस करने के कारण अपरिभाषित व्यवहार हो सकता है।

https://code.i-harness.com


  1. आप वास्तव में कुछ प्रकार के मूल्यवर्धक वस्तुओं की अपरिवर्तनीय वस्तुओं को चाहते हैं (जैसा कि आप रनटाइम प्रदर्शन की परवाह करते हैं और ढेर से बचना चाहते हैं)। बस सभी डेटा सदस्यों के साथ एक struct को public परिभाषित करें।

    struct Immutable {
        const std::string str;
        const int i;
    };

    आप उन्हें त्वरित रूप से कॉपी और कॉपी कर सकते हैं, डेटा सदस्यों को पढ़ सकते हैं, लेकिन यह इसके बारे में है। एक और एक प्रतियों के एक संदर्भ के संदर्भ से एक उदाहरण का निर्माण करना।

    Immutable obj1{"...", 42};
    Immutable obj2 = obj1;
    Immutable obj3 = std::move(obj1); // Copies, too
    
    obj3 = obj2; // Error, cannot assign

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

  2. आप चाहते हैं 1., अभी भी मूल्य शब्दार्थ के साथ, लेकिन थोड़ा आराम (जैसे कि वस्तुएं वास्तव में अब अपरिवर्तनीय नहीं हैं) और आप भी चिंतित हैं कि रनटाइम प्रदर्शन के लिए आपको कदम-निर्माण की आवश्यकता है। private डेटा सदस्यों और गेट्टर सदस्य कार्यों के आसपास कोई रास्ता नहीं है:

    class Immutable {
       public:
          Immutable(std::string str, int i) : str{std::move(str)}, i{i} {}
    
          const std::string& getStr() const { return str; }
          int getI() const { return i; }
    
       private:
          std::string str;
          int i;
    };

    उपयोग समान है, लेकिन चाल निर्माण वास्तव में चलता है।

    Immutable obj1{"...", 42};
    Immutable obj2 = obj1;
    Immutable obj3 = std::move(obj1); // Ok, does move-construct members

    आप असाइनमेंट को अनुमति देना चाहते हैं या नहीं यह अभी आपके नियंत्रण में नहीं है। यदि आप नहीं चाहते हैं तो बस असाइनमेंट ऑपरेटरों को = delete दें, अन्यथा कंपाइलर-जनरेट के साथ जाएं या अपना स्वयं का कार्यान्वयन करें।

    obj3 = obj2; // Ok if not manually disabled
  3. आप मूल्य शब्दार्थ के बारे में परवाह नहीं करते हैं और / या परमाणु संदर्भ गणना वृद्धि आपके परिदृश्य में ठीक हैं। @ NathanOliver के उत्तर में दर्शाए गए समाधान का उपयोग करें।


C ++ की सार्वभौमिक मूल्य शब्दार्थ विज्ञान की वजह से C ++ में अपरिवर्तनीयता को अधिकांश अन्य लोकप्रिय भाषाओं में अपरिवर्तनीयता की तुलना में सीधे नहीं किया जा सकता है। आपको यह पता लगाना होगा कि आप "अपरिवर्तनीय" का क्या मतलब चाहते हैं।

आप अन्य प्रकार के चर में नए मान निर्दिष्ट करने में सक्षम होना चाहते हैं। यह समझ में आता है, क्योंकि आप ऐसा कर सकते हैं कि C # में ImmutableObject प्रकार के चर के साथ।

उस मामले में, आप चाहते हैं कि शब्दार्थ पाने का सबसे सरल तरीका है

struct OtherImmutableObject {
    int i1;
    int i2;
};

ऐसा लग सकता है कि यह परस्पर है। आखिरकार, आप लिख सकते हैं

OtherImmutableObject x{1, 2};
x.i1 = 3;

लेकिन उस दूसरी पंक्ति का प्रभाव (संक्षिप्तता की अनदेखी कर रहा है ...) के प्रभाव के बिल्कुल समान है

x = OtherImmutableObject{3, x.i2};

इसलिए यदि आप अन्य OtherImmutableObject के प्रकार के असाइनमेंट को अनुमति देना चाहते हैं, तो इससे सदस्यों को सीधे असाइनमेंट को अस्वीकार करने का कोई मतलब नहीं है, क्योंकि यह कोई अतिरिक्त शब्दार्थ गारंटी प्रदान नहीं करता है; यह सब करता है एक ही अमूर्त ऑपरेशन धीमी के लिए कोड बनाते हैं। (इस मामले में, अधिकांश अनुकूलन कंपाइलर शायद दोनों अभिव्यक्तियों के लिए एक ही कोड उत्पन्न करेंगे, लेकिन यदि कोई सदस्य एक std::string वे ऐसा करने के लिए पर्याप्त स्मार्ट नहीं हो सकते हैं।)

ध्यान दें कि यह मूल रूप से C ++ में मूल रूप से प्रत्येक मानक प्रकार का व्यवहार है, जिसमें int , std::complex , std::string इत्यादि शामिल हैं। वे सभी इस अर्थ में परस्पर जुड़े हुए हैं कि आप उन्हें नए मान दे सकते हैं, और सभी अपरिवर्तनीय हैं यह समझ लें कि उन्हें बदलने के लिए केवल एक चीज (अमूर्त रूप से) आपको उनके लिए नए मूल्य प्रदान कर सकती है, जैसे कि C # में अपरिवर्तनीय संदर्भ प्रकार।

यदि आप उस शब्दार्थ को नहीं चाहते हैं, तो आपका एकमात्र विकल्प दूसरा कार्य करना है। मैं ऐसा करने की सलाह दूंगा कि अपने चरों को const घोषित करके, न कि सभी प्रकार के सदस्यों को कॉन्स्टेबल घोषित करके, क्योंकि यह आपको क्लास का उपयोग करने के लिए अधिक विकल्प देता है। उदाहरण के लिए, आप कक्षा का एक प्रारंभिक रूप से उत्परिवर्तित उदाहरण बना सकते हैं, उसमें एक मान का निर्माण कर सकते हैं, फिर उसके बाद केवल const संदर्भों का उपयोग करके इसे "फ्रीज" कर सकते हैं - जैसे string को StringBuilder , लेकिन इसे कॉपी करने के ओवरहेड के बिना।

(सभी सदस्यों को OtherImmutableObject const& घोषित करने का एक संभावित कारण यह हो सकता है कि यह कुछ मामलों में बेहतर अनुकूलन की अनुमति देता है। उदाहरण के लिए, यदि किसी फ़ंक्शन को एक अन्य OtherImmutableObject const& , और कंपाइलर कॉल साइट नहीं देख सकता है, तो यह सुरक्षित नहीं है। अन्य अज्ञात कोड पर कॉल करने के लिए सदस्यों के मूल्यों को कैश करने के लिए, क्योंकि अंतर्निहित ऑब्जेक्ट में const क्वालिफायर नहीं हो सकता है। लेकिन अगर वास्तविक सदस्यों को const घोषित किया जाता है, तो मुझे लगता है कि मूल्यों को कैश करना सुरक्षित होगा।)


C ++ में किसी वर्ग को अपरिवर्तनीय या कास्ट के रूप में पूर्वनिर्धारित करने की क्षमता नहीं है

और कुछ बिंदु पर आप शायद इस निष्कर्ष पर पहुंचेंगे कि आपको C ++ में वर्ग के सदस्यों के लिए const उपयोग नहीं करना चाहिए। यह सिर्फ झुंझलाहट के लायक नहीं है, और ईमानदारी से आप इसके बिना कर सकते हैं।

एक व्यावहारिक समाधान के रूप में, मैं कोशिश करूंगा:

typedef class _some_SUPER_obtuse_CLASS_NAME_PLEASE_DONT_USE_THIS { } const Immutable;

किसी को भी कुछ का उपयोग करने से हतोत्साहित करना लेकिन उनके कोड में Immutable


C ++ में बस ऐसा करने की आवश्यकता नहीं है:

class ImmutableObject {
    const int i1;
    const int i2;
}
ImmutableObject o1:
ImmutableObject o2;
o1 = o2; // Doesn't compile because immutable objects are not mutable.

यदि आप किसी अपरिवर्तनीय / कॉन्स्टेबल ऑब्जेक्ट का एक परस्पर संदर्भ चाहते हैं, तो आप एक पॉइंटर, एक स्मार्ट पॉइंटर, या एक reference_wrapper । जब तक आप वास्तव में एक ऐसा वर्ग रखना चाहते हैं, जिसकी सामग्री किसी भी समय किसी भी व्यक्ति द्वारा बदल दी जा सकती है, जो एक अपरिवर्तनीय वर्ग के विपरीत है।

* बेशक, C ++ एक ऐसी भाषा है जहाँ "नहीं" मौजूद नहीं है। उन कीमती कुछ सही मायने में असाधारण परिस्थितियों में आप const_cast उपयोग कर सकते हैं।


अपने प्रश्न का उत्तर देने के लिए, आप C ++ में अपरिवर्तनीय डेटा स्ट्रक्चर्स नहीं बनाते हैं क्योंकि const आईएनजी संपूर्ण ऑब्जेक्ट को संदर्भित करता है। नियम का उल्लंघन const_cast एस की उपस्थिति से दिखाई देता है।

अगर मैं केल्विन हेनी के "तुल्यकालन चतुर्भुज के बाहर सोच" का उल्लेख कर सकता हूं, तो इसके बारे में पूछने के लिए दो प्रश्न हैं:

  • क्या एक संरचना अपरिवर्तनीय या परिवर्तनशील है?
  • क्या यह साझा या साझा नहीं है?

इन सवालों को 4 ट्रैक्टरों के साथ एक अच्छी 2x2 तालिका में व्यवस्थित किया जा सकता है। समवर्ती संदर्भ में, केवल एक चतुर्थांश को सिंक्रनाइज़ेशन की आवश्यकता होती है: साझा किए जाने योग्य परस्पर डेटा।

वास्तव में, अपरिवर्तनीय डेटा को सिंक्रनाइज़ करने की आवश्यकता नहीं है क्योंकि आप इसे नहीं लिख सकते हैं, और समवर्ती रीड ठीक हैं। अनसर्डेड डेटा को सिंक्रनाइज़ करने की आवश्यकता नहीं है, क्योंकि केवल डेटा का स्वामी ही इसे लिख सकता है या इससे पढ़ सकता है।

तो डेटा संरचना के लिए यह ठीक है कि एक साझा संदर्भ में परिवर्तनशील हो, और अपरिवर्तनीयता का लाभ केवल एक साझा संदर्भ में होता है।

IMO, जो समाधान आपको सबसे अधिक स्वतंत्रता देता है, वह है आपकी कक्षा को उत्परिवर्तन और अपरिवर्तनीयता दोनों के लिए परिभाषित करना, निरंतरता का उपयोग करना जहाँ यह समझ में आता है (डेटा जो कि initalized है तो कभी नहीं बदला):

/* const-correct */ class C {
   int f1_;
   int f2_;

   const int f3_; // Semantic constness : initialized and never changed.
};

तब आप अपनी कक्षा C उदाहरणों को या तो परस्पर या अपरिवर्तनीय रूप से उपयोग कर सकते हैं, जो कि किसी भी मामले में, जहां-जहां-जहां-जहां-तहां समझदारी का फायदा पहुंचाते हैं।

यदि अब आप अपनी वस्तु साझा करना चाहते हैं, तो आप इसे एक स्मार्ट पॉइंटर में पैक कर सकते हैं:

shared_ptr<const C> ptr = make_shared<const C>(f1, f2, f3);

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


आप मूल रूप से एक std::unique_ptr या std::shared_ptr std::unique_ptr लाभ उठाकर जो चाहें प्राप्त कर सकते हैं। यदि आप केवल इन वस्तुओं में से एक चाहते हैं, लेकिन इसे चारों ओर ले जाने की अनुमति देते हैं, तो आप एक std::unique_ptr उपयोग कर सकते हैं। यदि आप कई वस्तुओं ("प्रतियों") के लिए अनुमति देना चाहते हैं, जिनमें सभी का मूल्य समान है, तो आप एक std::shared_Ptr उपयोग कर सकते हैं। नाम को छोटा करने के लिए एक उपनाम का उपयोग करें और एक कारखाना फ़ंक्शन प्रदान करें और यह बहुत दर्द रहित हो जाता है। इससे आपका कोड ऐसा दिखेगा:

class ImmutableClassImpl {
public: 
    const int i;
    const OtherImmutableClass o;
    const ReadOnlyCollection<OtherImmutableClass> r;

    public ImmutableClassImpl(int i, OtherImmutableClass o, 
        ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}

using Immutable = std::unique_ptr<ImmutableClassImpl>;

template<typename... Args>
Immutable make_immutable(Args&&... args)
{
    return std::make_unique<ImmutableClassImpl>(std::forward<Args>(args)...);
}

int main()
{
    auto first = make_immutable(...);
    // first points to a unique object now
    // can be accessed like
    std::cout << first->i;
    auto second = make_immutable(...);
    // now we have another object that is separate from first
    // we can't do
    // second = first;
    // but we can transfer like
    second = std::move(first);
    // which leaves first in an empty state where you can give it a new object to point to
}

यदि कोड इसके बजाय एक shared_ptr उपयोग करने के लिए परिवर्तन है तो आप कर सकते हैं

second = first;

और फिर दोनों ऑब्जेक्ट एक ही ऑब्जेक्ट को इंगित करते हैं, लेकिन न तो इसे संशोधित कर सकते हैं।


मैं कहूंगा कि सबसे मुहावरेदार तरीका यह होगा:

struct OtherImmutable {
    int i1;
    int i2;

    OtherImmutable(int i1, int i2) : i1(i1), i2(i2) {}
};

लेकिन ... यह अपरिवर्तनीय नहीं है ??

वास्तव में, लेकिन आप इसे मूल्य के रूप में पास कर सकते हैं:

void frob1() {
    OtherImmutable oi;
    oi = frob2(oi);
}

auto frob2(OtherImmutable oi) -> OtherImmutable {
    // cannot affect frob1 oi, since it's a copy
}

और भी बेहतर, ऐसे स्थान जिन्हें स्थानीय रूप से म्यूट करने की आवश्यकता नहीं है वे अपने स्थानीय चरों को कॉन्स्टेबल के रूप में परिभाषित कर सकते हैं:

auto frob2(OtherImmutable const oi) -> OtherImmutable {
    return OtherImmutable{oi.i1 + 1, oi.i2};
}

सूचक शब्दार्थ के साथ अपरिवर्तनीय वस्तुएं बहुत बेहतर काम करती हैं। तो एक स्मार्ट अपरिवर्तनीय सूचक लिखें:

struct immu_tag_t {};
template<class T>
struct immu:std::shared_ptr<T const>
{
  using base = std::shared_ptr<T const>;

  immu():base( std::make_shared<T const>() ) {}

  template<class A0, class...Args,
    std::enable_if_t< !std::is_base_of< immu_tag_t, std::decay_t<A0> >{}, bool > = true,
    std::enable_if_t< std::is_construtible< T const, A0&&, Args&&... >{}, bool > = true
  >
  immu(A0&& a0, Args&&...args):
    base(
      std::make_shared<T const>(
        std::forward<A0>(a0), std::forward<Args>(args)...
      )
    )
  {}
  template<class A0, class...Args,
    std::enable_if_t< std::is_construtible< T const, std::initializer_list<A0>, Args&&... >{}, bool > = true
  >
  immu(std::initializer_list<A0> a0, Args&&...args):
    base(
      std::make_shared<T const>(
        a0, std::forward<Args>(args)...
      )
    )
  {}

  immu( immu_tag_t, std::shared_ptr<T const> ptr ):base(std::move(ptr)) {}
  immu(immu&&)=default;
  immu(immu const&)=default;
  immu& operator=(immu&&)=default;
  immu& operator=(immu const&)=default;

  template<class F>
  immu modify( F&& f ) const {
    std::shared_ptr<T> ptr;
    if (!*this) {
      ptr = std::make_shared<T>();
    } else {
      ptr = std::make_shared<T>(**this);
    }
    std::forward<F>(f)(*ptr);
    return {immu_tag_t{}, std::move(ptr)};
  }
};

यह अपने कार्यान्वयन के अधिकांश के लिए shared_ptr लाभ उठाता है; shared_ptr अधिकांश नुकसान अपरिवर्तनीय वस्तुओं के साथ कोई समस्या नहीं है।

साझा ptr के विपरीत, यह आपको सीधे ऑब्जेक्ट बनाने की अनुमति देता है, और डिफ़ॉल्ट रूप से एक गैर-अशक्त स्थिति बनाता है। यह अभी भी स्थानांतरित होने से एक अशक्त अवस्था तक पहुँच सकता है। आप निम्न स्थिति में से एक बना सकते हैं:

immu<int> immu_null_int{ immu_tag_t{}, {} };

और एक गैर अशक्त int के माध्यम से:

immu<int> immu_int;

या

immu<int> immu_int = 7;

मैंने एक उपयोगी उपयोगिता विधि जोड़ी, जिसे modify कहा जाता है। संशोधित करने से पहले आपको एक immu<T> में पैक किया गया लौटाया जाता है, संशोधित करने के लिए एक लंबो पास करने के लिए T का एक परस्पर उदाहरण देता है।

कंक्रीट का उपयोग जैसा दिखता है:

struct data;
using immu_data = immu<data>;
struct data {
  int i;
  other_immutable_class o;
  std::vector<other_immutable_class> r;
  data( int i_in, other_immutable_class o_in, std::vector<other_immutable_class> r_in ):
    i(i_in), o(std::move(o_in)), r( std::move(r_in))
  {}
};

फिर immu_data उपयोग immu_data

एक्सेस करने वाले सदस्यों की आवश्यकता है -> नहीं . , और आप null immu_data s के लिए जाँच करें यदि आप उन्हें पारित कर रहे हैं।

यहां बताया गया है कि आप कैसे उपयोग .modify

immu_data a( 7, other_immutable_class{}, {} );
immu_data b = a.modify([&](auto& b){ ++b.i; b.r.emplace_back() });

यह एक b बनाता है जिसका मान 1 के बराबर a , सिवाय इसके कि i 1 से वेतन वृद्धि हुई है, और एक अतिरिक्त other_immutable_class in br (डिफ़ॉल्ट रूप से निर्मित) है। ध्यान दें कि a , b बनाकर असंशोधित है।

ऊपर शायद टाइपो हैं, लेकिन मैंने डिज़ाइन का उपयोग किया है।

यदि आप फैंसी प्राप्त करना चाहते हैं, तो आप अनूठे समर्थन को कॉपी-ऑन-राइट या अद्वितीय होने पर संशोधित कर सकते हैं। हालांकि यह लगता है की तुलना में कठिन है।







const-cast