क्यों std :: share_ptr <void> काम करते हैं


129

मुझे शटडाउन पर मनमाने ढंग से सफाई करने के लिए std :: share_ptr का उपयोग करके कुछ कोड मिला। पहले मुझे लगा कि यह कोड संभवतः काम नहीं कर सकता, लेकिन फिर मैंने निम्नलिखित कोशिश की:

#include <memory>
#include <iostream>
#include <vector>

class test {
public:
  test() {
    std::cout << "Test created" << std::endl;
  }
  ~test() {
    std::cout << "Test destroyed" << std::endl;
  }
};

int main() {
  std::cout << "At begin of main.\ncreating std::vector<std::shared_ptr<void>>" 
            << std::endl;
  std::vector<std::shared_ptr<void>> v;
  {
    std::cout << "Creating test" << std::endl;
    v.push_back( std::shared_ptr<test>( new test() ) );
    std::cout << "Leaving scope" << std::endl;
  }
  std::cout << "Leaving main" << std::endl;
  return 0;
}

यह कार्यक्रम आउटपुट देता है:

At begin of main.
creating std::vector<std::shared_ptr<void>>
Creating test
Test created
Leaving scope
Leaving main
Test destroyed

मेरे पास कुछ विचार हैं कि यह क्यों काम कर सकता है, जो कि जीएच ++ के लिए लागू किए गए std :: साझा_ptrs के आंतरिक के साथ करना है। चूँकि ये ऑब्जेक्ट आंतरिक सूचक को एक साथ काउंटर से लपेटते हैं, std::shared_ptr<test>जिससे std::shared_ptr<void>कि शायद विध्वंसक की कॉल में बाधा नहीं होती है। क्या यह धारणा सही है?

और निश्चित रूप से बहुत अधिक महत्वपूर्ण सवाल: क्या यह मानक द्वारा काम करने की गारंटी है, या आगे std के आंतरिक में परिवर्तन हो सकता है :: share_ptr, अन्य कार्यान्वयन वास्तव में इस कोड को तोड़ते हैं?


2
आपको इसके बजाय क्या होने की उम्मीद थी?
लाइट मेन्स रेस ऑर्बिट में

1
वहां कोई कास्ट नहीं है - यह shared_ptr <test> से shared_ptr <void> में रूपांतरण है।
एलन स्टोक्स

FYI करें: एमएसडीएन में std :: share_ptr के बारे में एक लेख का लिंक यहां दिया गया है: msdn.microsoft.com/en-us/library/bb982026.aspx और यह GCC से प्रलेखन है: gcc .gnuu
yasouser

जवाबों:


98

चाल है कि std::shared_ptrप्रकार erasure प्रदर्शन करता है। मूल रूप से, जब कोई नया shared_ptrबनाया जाता है , तो यह आंतरिक रूप से एक deleterफ़ंक्शन (जिसे कंस्ट्रक्टर को तर्क के रूप में दिया जा सकता है, लेकिन कॉल करने के लिए चूक मौजूद नहीं है delete) संग्रहीत करेगा । जब shared_ptrनष्ट हो जाता है, तो यह उस संग्रहित फ़ंक्शन को कॉल करता है और जो कॉल को कॉल करेगा deleter

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

template <typename T>
void delete_deleter( void * p ) {
   delete static_cast<T*>(p);
}

template <typename T>
class my_unique_ptr {
  std::function< void (void*) > deleter;
  T * p;
  template <typename U>
  my_unique_ptr( U * p, std::function< void(void*) > deleter = &delete_deleter<U> ) 
     : p(p), deleter(deleter) 
  {}
  ~my_unique_ptr() {
     deleter( p );   
  }
};

int main() {
   my_unique_ptr<void> p( new double ); // deleter == &delete_deleter<double>
}
// ~my_unique_ptr calls delete_deleter<double>(p)

जब किसी shared_ptrअन्य से हटाए गए (या डिफ़ॉल्ट रूप से बनाए गए) को हटा दिया जाता है, तो जब आप कॉल करने के लिए विध्वंसक पर जानकारी shared_ptr<T>से निर्माण करते हैं, तो shared_ptr<U>उसके आसपास भी पास हो जाता है deleter


एक गलत धारणा लगती है my_shared:। मैं इसे ठीक कर दूँगा लेकिन अभी तक संपादित करने का कोई विशेषाधिकार नहीं है।
एलेक्सी कुकानोव

@Alexey कुकानोव, @ डेनिस ज़िकफ़ोज़: संपादन के लिए धन्यवाद मैं दूर था और इसे नहीं देखा।
डेविड रॉड्रिग्ज -

2
@ user102008 आपको 'std :: function' की आवश्यकता नहीं है, लेकिन यह थोड़ा अधिक लचीला है (शायद यहां कोई फर्क नहीं पड़ता), लेकिन यह इस बात को नहीं बदलता है कि इरेज़र कैसे काम करता है, यदि आप 'delete_deleter <T>' को स्टोर करते हैं तो फ़ंक्शन पॉइंटर 'शून्य (शून्य *)' आप वहाँ इरेज़र टाइप कर रहे हैं: टी स्टोर किए गए पॉइंटर प्रकार से चला गया है।
डेविड रोड्रिग्ज - 10

1
यह व्यवहार C ++ मानक की गारंटी है, है ना? मुझे अपनी एक कक्षा में टाइप इरेज़र की आवश्यकता है, और std::shared_ptr<void>मुझे एक बेकार रैपर क्लास घोषित करने से बचने की अनुमति देता है ताकि मैं इसे एक निश्चित आधार वर्ग से प्राप्त कर सकूं।
वायलेट जिराफ

1
@AngelusMortis: सटीक डिलेटर के प्रकार का हिस्सा नहीं है my_unique_ptr। जब mainटेम्प्लेट में doubleझटपट को सही डिलेटर के साथ चुना जाता है, लेकिन यह प्रकार का हिस्सा नहीं होता है my_unique_ptrऔर इसे ऑब्जेक्ट से पुनर्प्राप्त नहीं किया जा सकता है। डिलेटर के प्रकार को ऑब्जेक्ट से मिटा दिया जाता है, जब कोई फ़ंक्शन एक my_unique_ptr(rvalue-reference द्वारा कहे) प्राप्त करता है , तो वह फ़ंक्शन नहीं जानता है और यह जानने की आवश्यकता नहीं है कि डीलेटर क्या है।
डेविड रॉड्रिग्ज - dribeas

35

shared_ptr<T> तार्किक रूप से [*] के पास कम से कम दो प्रासंगिक डेटा सदस्य हैं:

  • ऑब्जेक्ट के लिए एक संकेतक प्रबंधित किया जा रहा है
  • डिलेटर फ़ंक्शन के लिए एक संकेतक जो इसे नष्ट करने के लिए उपयोग किया जाएगा।

अपने की Deleter समारोह shared_ptr<Test>, जिस तरह से आप यह निर्माण को देखते हुए, के लिए सामान्य से एक है Test, जो करने के लिए सूचक धर्मान्तरित Test*और deleteयह है।

जब आप अपने shared_ptr<Test>वेक्टर के अंदर धकेलते हैं shared_ptr<void>, तो उन दोनों को कॉपी किया जाता है, हालांकि पहले वाले को बदल दिया जाता है void*

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

यह वास्तव में एक छोटे से अधिक इस से जटिल है, क्योंकि shared_ptrएक Deleter ले जा सकते हैं functor सिर्फ एक फ़ंक्शन के बजाय, तो भी प्रति-वस्तु डेटा सिर्फ एक समारोह सूचक के बजाय संग्रहीत करने के लिए हो सकता है। लेकिन इस मामले के लिए ऐसा कोई अतिरिक्त डेटा नहीं है, यह केवल एक टेम्पलेट फ़ंक्शन के तात्कालिकता के लिए एक पॉइंटर को स्टोर करने के लिए पर्याप्त होगा, एक टेम्पलेट पैरामीटर के साथ जो उस प्रकार को कैप्चर करता है जिसके माध्यम से पॉइंटर को हटाना होगा।

[*] तार्किक रूप से इस अर्थ में कि उनके पास इसकी पहुंच है - वे स्वयं share_ptr के सदस्य नहीं हो सकते, बल्कि कुछ प्रबंधन नोड के बजाय जो इसे इंगित करते हैं।


2
+1 उल्लेख करने के लिए कि डिलेटर फ़ंक्शन / फ़ंक्टर को अन्य शेयर्ड_प्ट्र इंस्टेंस में कॉपी किया जाता है - जानकारी का एक टुकड़ा अन्य उत्तरों में छूट गया।
एलेक्सी कुकानोव

क्या इसका मतलब यह है कि share_ptrs का उपयोग करते समय वर्चुअल बेस डिस्ट्रक्टर्स की आवश्यकता नहीं होती है?
6:52

@ क्रोनाग हाँ। हालाँकि, मैं अभी भी विध्वंसक को आभासी बनाने की सलाह दूंगा, अगर आपके पास कोई अन्य आभासी सदस्य है तो कम से कम। (किसी भी संभावित लाभ से एक बार गलती से भूल जाने का दर्द।)
एलन स्टोक्स

हां, मैं मानूंगा। दिलचस्प गैर-कम। मुझे पता था कि प्रकार के क्षरण के बारे में सिर्फ इस "विशेषता" पर विचार नहीं किया गया था।
11'11

2
@ क्रोनोग्रफ़: यदि आप shared_ptrसीधे उपयुक्त प्रकार से या यदि आप उपयोग करते हैं तो वर्चुअल विध्वंसक की आवश्यकता नहीं है make_shared। लेकिन, फिर भी यह एक अच्छा विचार है क्योंकि पॉइंटर का प्रकार निर्माण से बदल सकता है जब तक कि इसमें संग्रहीत नहीं किया जाता है shared_ptr: base *p = new derived; shared_ptr<base> sp(p);जहां तक shared_ptrचिंतित है कि वस्तु baseनहीं है derived, इसलिए आपको एक आभासी विध्वंसक की आवश्यकता है। यह पैटर्न कारखाने के पैटर्न के साथ सामान्य हो सकता है, उदाहरण के लिए।
डेविड रॉड्रिग्ज -

10

यह काम करता है क्योंकि यह प्रकार के क्षरण का उपयोग करता है।

असल में, जब आप एक निर्माण करते हैं shared_ptr, तो यह एक अतिरिक्त तर्क देता है (कि आप वास्तव में यदि आप चाहें तो प्रदान कर सकते हैं), जो कि डिलेटर फंक्टर है।

यह डिफ़ॉल्ट फ़ंक्टर आपके द्वारा उपयोग किए जाने वाले प्रकार के लिए एक संकेतक के रूप में तर्क को स्वीकार करता है shared_ptr, इस प्रकार void, यहां आपके द्वारा उपयोग किए गए स्थिर प्रकार के लिए इसे उचित रूप से testडाला जाता है, और इस ऑब्जेक्ट पर विध्वंसक को कॉल करता है।

किसी भी पर्याप्त रूप से उन्नत विज्ञान जादू की तरह लगता है, है ना?


5

कंस्ट्रक्टर shared_ptr<T>(Y *p)वास्तव में कॉल करने लगता है shared_ptr<T>(Y *p, D d)जहां dऑब्जेक्ट के लिए स्वचालित रूप से उत्पन्न डेलेटर है।

जब ऐसा होता Yहै, तो ऑब्जेक्ट का प्रकार ज्ञात होता है, इसलिए इस shared_ptrऑब्जेक्ट के डीलेटर को पता होता है कि कॉल करने के लिए कौन-सा डिस्ट्रक्टर है और पॉइंटर के वेक्टर में संग्रहीत होने पर यह जानकारी खो नहीं जाती है shared_ptr<void>

वास्तव में चश्मा की आवश्यकता होती है कि किसी shared_ptr<T>वस्तु को स्वीकार करने के लिए किसी प्राप्य वस्तु के लिए shared_ptr<U>यह सत्य U*होना चाहिए और इसका एक के लिए एक रूपांतर होना चाहिए T*और यह निश्चित रूप से ऐसा होता है T=voidक्योंकि किसी भी सूचक को void*अव्यवस्थित रूप से परिवर्तित किया जा सकता है । डिलेटर के बारे में कुछ भी नहीं कहा जाता है जो कि अमान्य होगा इसलिए वास्तव में चश्मा अनिवार्य कर रहे हैं कि यह सही तरीके से काम करेगा।

तकनीकी रूप से IIRC एक shared_ptr<T>छिपे हुए ऑब्जेक्ट के लिए एक पॉइंटर रखता है जिसमें संदर्भ काउंटर और वास्तविक ऑब्जेक्ट के लिए एक पॉइंटर होता है; इस छिपी हुई संरचना में डिलेटर को संचय करके, यह संभव है कि यह स्पष्ट रूप से जादू की सुविधा के लिए काम कर रहा है, जबकि shared_ptr<T>एक नियमित पॉइंटर के रूप में बड़ा है (हालांकि पॉइंटर को डीरिनर करने के लिए एक डबल अप्रत्यक्षता की आवश्यकता होती है)

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*यह स्पष्ट रूप से परिवर्तनीय है void*, इसलिए shared_ptr<Test>अंतर्निहित रूप shared_ptr<void>से स्मृति से परिवर्तनीय है । यह काम करता है क्योंकि shared_ptrरन-टाइम में विनाश को नियंत्रित करने के लिए डिज़ाइन किया गया है, न कि संकलन-समय पर, वे आंतरिक रूप से विरासत का उपयोग करेंगे उचित डिस्ट्रक्टर को कॉल करने के लिए क्योंकि यह आवंटन के समय था।


क्या आप और अधिक व्याख्या कर सकते हैं? मैंने अभी इसी तरह का प्रश्न पोस्ट किया है, यह बहुत अच्छा होगा यदि आप मदद कर सकते हैं!
ब्रूस

3

मैं इस प्रश्न का उत्तर देने जा रहा हूं (2 साल बाद) शेयर्ड_प्ट्र के एक बहुत ही सरलीकृत कार्यान्वयन का उपयोग करके जो उपयोगकर्ता समझेगा।

सबसे पहले मैं कुछ साइड क्लासेस, share_ptr_base, sp_counted_base sp_counted_impl, और check_deleter के अंतिम भाग में जा रहा हूँ, जिनमें से एक टेम्पलेट है।

class sp_counted_base
{
 public:
    sp_counted_base() : refCount( 1 )
    {
    }

    virtual ~sp_deleter_base() {};
    virtual void destruct() = 0;

    void incref(); // increases reference count
    void decref(); // decreases refCount atomically and calls destruct if it hits zero

 private:
    long refCount; // in a real implementation use an atomic int
};

template< typename T > class sp_counted_impl : public sp_counted_base
{
 public:
   typedef function< void( T* ) > func_type;
    void destruct() 
    { 
       func(ptr); // or is it (*func)(ptr); ?
       delete this; // self-destructs after destroying its pointer
    }
   template< typename F >
   sp_counted_impl( T* t, F f ) :
       ptr( t ), func( f )

 private:

   T* ptr; 
   func_type func;
};

template< typename T > struct checked_deleter
{
  public:
    template< typename T > operator()( T* t )
    {
       size_t z = sizeof( T );
       delete t;
   }
};

class shared_ptr_base
{
private:
     sp_counted_base * counter;

protected:
     shared_ptr_base() : counter( 0 ) {}

     explicit shared_ptr_base( sp_counter_base * c ) : counter( c ) {}

     ~shared_ptr_base()
     {
        if( counter )
          counter->decref();
     }

     shared_ptr_base( shared_ptr_base const& other )
         : counter( other.counter )
     {
        if( counter )
            counter->addref();
     }

     shared_ptr_base& operator=( shared_ptr_base& const other )
     {
         shared_ptr_base temp( other );
         std::swap( counter, temp.counter );
     }

     // other methods such as reset
};

अब मैं make_sp_counted_impl नामक दो "फ्री" फंक्शन बनाने जा रहा हूं, जो एक पॉइंटर को नए बनाए गए एक पर लौटाएगा।

template< typename T, typename F >
sp_counted_impl<T> * make_sp_counted_impl( T* ptr, F func )
{
    try
    {
       return new sp_counted_impl( ptr, func );
    }
    catch( ... ) // in case the new above fails
    {
        func( ptr ); // we have to clean up the pointer now and rethrow
        throw;
    }
}

template< typename T > 
sp_counted_impl<T> * make_sp_counted_impl( T* ptr )
{
     return make_sp_counted_impl( ptr, checked_deleter<T>() );
}

ठीक है, ये दो कार्य आवश्यक हैं जैसे कि जब आप एक templated फ़ंक्शन के माध्यम से साझा करें, तो आगे क्या होगा।

template< typename T >
class shared_ptr : public shared_ptr_base
{

 public:
   template < typename U >
   explicit shared_ptr( U * ptr ) :
         shared_ptr_base( make_sp_counted_impl( ptr ) )
   {
   }

  // implement the rest of shared_ptr, e.g. operator*, operator->
};

ध्यान दें कि यदि टी शून्य है और ऊपर यू आपके "परीक्षण" वर्ग है तो क्या होगा। यह Make_sp_counted_impl () को U के पॉइंटर के साथ कहता है, T को पॉइंटर नहीं। विनाश का प्रबंधन सभी यहां से किया जाता है। Share_ptr_base वर्ग प्रतिलिपि और असाइनमेंट आदि के संदर्भ के साथ संदर्भ की गिनती का प्रबंधन करता है। साझा किया गया साझा वर्ग ऑपरेटर ओवरलोड (->, * आदि) के प्रकार के उपयोग का प्रबंधन करता है।

इस प्रकार यद्यपि आपके पास साझा करने के लिए एक साझा_प्रति है, पर आप उस प्रकार के एक संकेतक का प्रबंधन कर रहे हैं जिसे आपने नए में पारित किया है। ध्यान दें कि यदि आप अपने पॉइंटर को एक शून्य * में परिवर्तित करते हैं, तो इसे share_ptr में डालने से पहले, यह check_delete पर संकलित करने में विफल हो जाएगा, इसलिए आप वास्तव में वहां भी सुरक्षित हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.