เหตุใด std :: shared_ptr <void> จึงทำงาน


129

ฉันพบรหัสบางส่วนโดยใช้ std :: shared_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

ฉันมีความคิดบางอย่างเกี่ยวกับสาเหตุที่อาจใช้งานได้ซึ่งเกี่ยวข้องกับ internals ของ std :: shared_ptrs ตามที่ใช้สำหรับ G ++ เนื่องจากวัตถุเหล่านี้ห่อตัวชี้ภายในพร้อมกับตัวนับการส่งจากstd::shared_ptr<test>ถึงstd::shared_ptr<void>อาจไม่ขัดขวางการเรียกของ destructor สมมติฐานนี้ถูกต้องหรือไม่

และแน่นอนคำถามที่สำคัญยิ่งกว่านี้คือสิ่งนี้รับประกันได้ว่าจะทำงานได้ตามมาตรฐานหรืออาจมีการเปลี่ยนแปลงภายในของ std :: shared_ptr เพิ่มเติมการใช้งานอื่น ๆ จะทำลายรหัสนี้หรือไม่


2
คุณคาดหวังว่าจะเกิดอะไรขึ้นแทน
การแข่งขัน Lightness ใน Orbit

1
ไม่มีการแปลง - เป็นการแปลงจาก shared_ptr <test> เป็น shared_ptr <void>
Alan Stokes

FYI: นี่คือลิงก์ไปยังบทความเกี่ยวกับ std :: shared_ptr ใน MSDN: msdn.microsoft.com/en-us/library/bb982026.aspxและนี่คือเอกสารจาก GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

คำตอบ:


98

เคล็ดลับคือstd::shared_ptrการลบประเภท โดยทั่วไปเมื่อมีการสร้างใหม่shared_ptrมันจะจัดเก็บภายในdeleterฟังก์ชั่น (ซึ่งสามารถกำหนดเป็นอาร์กิวเมนต์ให้กับตัวสร้าง แต่ถ้าไม่มีค่าเริ่มต้นที่จะเรียกdelete) เมื่อถูกทำลายมันเรียกว่าฟังก์ชั่นการจัดเก็บและที่จะเรียกshared_ptrdeleter

ภาพร่างแบบง่าย ๆ ของการลบประเภทที่เกิดขึ้นง่ายขึ้นด้วยฟังก์ชัน std :: และหลีกเลี่ยงการนับการอ้างอิงและปัญหาอื่น ๆ ได้ที่นี่:

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มีการคัดลอก (หรือเริ่มต้นสร้าง) จากที่อื่น Deleter จะถูกส่งไปรอบ ๆ เพื่อที่ว่าเมื่อคุณสร้างshared_ptr<T>จากshared_ptr<U>ข้อมูลเกี่ยวกับสิ่งที่ destructor deleterต่อการเรียกร้องจะถูกส่งยังรอบใน


ดูเหมือนจะมีการพิมพ์ผิด: my_shared. ฉันจะแก้ไข แต่ไม่มีสิทธิ์ในการแก้ไข
Alexey Kukanov

@Alexey Kukanov, @Dennis Zickefoose: ขอบคุณสำหรับการแก้ไขฉันไม่อยู่และไม่เห็น
David Rodríguez - dribeas

2
@ user102008 คุณไม่ต้องการ 'std :: function' แต่มีความยืดหยุ่นมากกว่า (อาจไม่สำคัญที่นี่เลย) แต่นั่นไม่ได้เปลี่ยนวิธีลบประเภทการทำงานถ้าคุณเก็บ 'delete_deleter <T>' เป็น ตัวชี้ฟังก์ชัน 'โมฆะ (โมฆะ *)' คุณกำลังทำการลบประเภทที่นั่น: T หายไปจากประเภทตัวชี้ที่เก็บไว้
David Rodríguez - dribeas

1
พฤติกรรมนี้รับประกันโดยมาตรฐาน C ++ ใช่มั้ย ฉันต้องการลบประเภทในชั้นเรียนของฉันและstd::shared_ptr<void>ให้ฉันหลีกเลี่ยงการประกาศคลาส wrapper ที่ไร้ประโยชน์เพียงเพื่อให้ฉันสามารถสืบทอดจากคลาสฐานที่แน่นอน
Violet Giraffe

1
@AngelusMortis: ผู้ Deleter my_unique_ptrที่แน่นอนไม่ได้เป็นส่วนหนึ่งของประเภทของ เมื่อmainเลือกเทมเพลตในเทมเพลตdoubleที่เหมาะสมแต่สิ่งนี้ไม่ได้เป็นส่วนหนึ่งของประเภทmy_unique_ptrและไม่สามารถเรียกคืนได้จากวัตถุ ประเภทของ deleter ถูกลบออกจากวัตถุเมื่อฟังก์ชั่นได้รับmy_unique_ptr(พูดโดยอ้างอิง rvalue) ฟังก์ชั่นที่ไม่ได้และไม่จำเป็นต้องรู้ว่า deleter คืออะไร
David Rodríguez - dribeas

35

shared_ptr<T> เหตุผล [*] มีสมาชิกข้อมูลที่เกี่ยวข้องสองคน (อย่างน้อย):

  • ตัวชี้ไปยังวัตถุที่มีการจัดการ
  • ตัวชี้ไปยังฟังก์ชัน deleter ที่จะใช้ในการทำลายมัน

ฟังก์ชั่น Deleter ที่คุณshared_ptr<Test>ได้รับในแบบที่คุณสร้างมันเป็นคนปกติTestซึ่งจะแปลงตัวชี้ไปTest*และdeletes มัน

เมื่อคุณผลักดันของคุณshared_ptr<Test>เป็นเวกเตอร์ของshared_ptr<void>, ทั้งvoid*ของผู้ที่จะถูกคัดลอกแม้จะเป็นคนแรกที่จะถูกแปลงเป็น

ดังนั้นเมื่อองค์ประกอบเวกเตอร์ถูกทำลายโดยใช้การอ้างอิงครั้งสุดท้ายมันจะส่งตัวชี้ไปยัง deleter ที่ทำลายมันอย่างถูกต้อง

มันเป็นเรื่องจริงที่ซับซ้อนน้อยกว่านี้มากขึ้นเพราะshared_ptrสามารถใช้ Deleter functorมากกว่าแค่ฟังก์ชั่นเพื่อให้มีแม้กระทั่งอาจจะมีข้อมูลต่อวัตถุจะถูกเก็บไว้มากกว่าแค่ตัวชี้ฟังก์ชัน แต่สำหรับกรณีนี้ไม่มีข้อมูลพิเศษดังกล่าวมันก็เพียงพอแล้วที่จะเก็บตัวชี้ไปยังอินสแตนซ์ของฟังก์ชันเทมเพลตโดยมีพารามิเตอร์เทมเพลตที่จับประเภทที่ต้องลบตัวชี้

[*] อย่างมีเหตุผลในแง่ที่มันเข้าถึงพวกเขา - พวกเขาอาจไม่ได้เป็นสมาชิกของ shared_ptr เอง แต่แทนที่จะเป็นโหนดการจัดการบางอย่างที่ชี้ไป


2
+1 สำหรับการกล่าวถึงว่าฟังก์ชั่น deleter / functor จะถูกคัดลอกไปยังอินสแตนซ์ shared_ptr อื่น ๆ ซึ่งเป็นข้อมูลที่ไม่ได้รับคำตอบ
Alexey Kukanov

สิ่งนี้หมายความว่าไม่ต้องการตัวทำลายฐานเสมือนเมื่อใช้ shared_ptrs?
ronag

@ronag ใช่ อย่างไรก็ตามฉันยังคงแนะนำให้สร้าง destructor เสมือนอย่างน้อยถ้าคุณมีสมาชิกเสมือนอื่น ๆ (ความเจ็บปวดจากการลืมโดยบังเอิญเมื่อเทียบกับผลประโยชน์ใด ๆ ที่เป็นไปได้)
Alan Stokes

ใช่ฉันจะเห็นด้วย ที่น่าสนใจไม่น้อย ฉันรู้เกี่ยวกับการลบประเภทไม่ได้พิจารณาคุณสมบัติ "" นี้
ronag

2
@ronag: destructors เสมือนไม่จำเป็นถ้าคุณสร้างโดยตรงกับประเภทที่เหมาะสมหรือถ้าคุณใช้shared_ptr make_sharedแต่ก็ยังเป็นความคิดที่ดีเนื่องจากชนิดของตัวชี้สามารถเปลี่ยนจากการสร้างได้จนกว่าจะถูกเก็บไว้ในshared_ptr: base *p = new derived; shared_ptr<base> sp(p);เท่าที่shared_ptrเกี่ยวข้องกับวัตถุนั้นbaseไม่ได้derivedดังนั้นคุณต้องมี destructor เสมือน รูปแบบนี้สามารถใช้ร่วมกับรูปแบบของโรงงานได้
David Rodríguez - dribeas

10

มันใช้งานได้เพราะมันใช้การลบประเภท

โดยทั่วไปเมื่อคุณสร้าง a shared_ptrมันจะส่งผ่านอาร์กิวเมนต์พิเศษหนึ่งข้อ (ซึ่งคุณสามารถระบุได้หากคุณต้องการ) ซึ่งเป็นนักแสดงตลก

ฟังก์ชั่นเริ่มต้นนี้ยอมรับเป็นอาร์กิวเมนต์ตัวชี้ที่จะพิมพ์คุณใช้ในที่นี่shared_ptrจึงvoidมันได้อย่างเหมาะสมกับประเภทคงที่คุณใช้testที่นี่และเรียก destructor บนวัตถุนี้

วิทยาศาสตร์ขั้นสูงใด ๆ ที่รู้สึกเหมือนเวทมนต์ใช่ไหม?


5

คอนสตรัคเตอร์shared_ptr<T>(Y *p)จริง ๆ แล้วดูเหมือนจะเรียกshared_ptr<T>(Y *p, D d)ว่าdเป็น deleter สร้างขึ้นโดยอัตโนมัติสำหรับวัตถุ

เมื่อเกิดเหตุการณ์นี้ประเภทของวัตถุที่Yเป็นที่รู้จักกันเพื่อ Deleter นี้สำหรับshared_ptrวัตถุรู้ซึ่ง destructor shared_ptr<void>โทรและข้อมูลนี้จะไม่ได้หายไปเมื่อตัวชี้จะถูกเก็บไว้ในเวกเตอร์ของ

แน่นอนว่ารายละเอียดต้องการให้shared_ptr<T>วัตถุที่รับshared_ptr<U>วัตถุนั้นจะต้องเป็นจริงและU*ต้องแปลงสภาพโดยปริยายเป็น a T*และนี่เป็นกรณีที่แน่นอนT=voidเพราะตัวชี้ใด ๆ สามารถถูกแปลงเป็นvoid*ปริยายได้ ไม่มีการพูดเกี่ยวกับ deleter ที่จะไม่ถูกต้องดังนั้นรายละเอียดที่แน่นอนนั้นจะทำงานได้อย่างถูกต้อง

เทคนิค IIRC a shared_ptr<T>ถือตัวชี้ไปยังวัตถุที่ซ่อนอยู่ซึ่งมีตัวนับการอ้างอิงและตัวชี้ไปยังวัตถุจริง โดยการจัดเก็บ deleter ในโครงสร้างที่ซ่อนอยู่นี้เป็นไปได้ที่จะทำให้คุณสมบัติเวทย์มนตร์นี้เห็นได้ชัดในขณะที่ยังคงรักษาตัวshared_ptr<T>ใหญ่เหมือนตัวชี้ปกติ

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*แปลงได้โดยปริยายเป็นvoid*ดังนั้นจึงshared_ptr<Test>แปลงได้โดยปริยายshared_ptr<void>จากหน่วยความจำ งานshared_ptrนี้ได้รับการออกแบบมาเพื่อควบคุมการทำลายในเวลาทำงานไม่ใช่การคอมไพล์พวกเขาจะใช้การสืบทอดภายในเพื่อเรียก destructor ที่เหมาะสมเหมือนในเวลาที่จัดสรร


คุณอธิบายเพิ่มเติมได้ไหม ฉันได้โพสต์คำถามที่คล้ายกันในตอนนี้มันจะดีถ้าคุณสามารถช่วยได้!
Bruce

3

ฉันจะตอบคำถามนี้ (2 ปีต่อมา) โดยใช้การใช้งานแบบง่ายๆของ shared_ptr ที่ผู้ใช้จะเข้าใจ

ประการแรกฉันจะเรียนสองสามด้าน shared_ptr_base, sp_counted_base sp_counted_impl และ checked_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>() );
}

ตกลงฟังก์ชั่นทั้งสองนี้มีความสำคัญต่อสิ่งที่จะเกิดขึ้นต่อไปเมื่อคุณสร้าง shared_ptr ผ่านฟังก์ชั่น 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->
};

สังเกตสิ่งที่เกิดขึ้นข้างต้นถ้า T เป็นโมฆะและ U คือคลาส "ทดสอบ" ของคุณ มันจะเรียก make_sp_counted_impl () พร้อมกับตัวชี้ไปที่ U ไม่ใช่ตัวชี้ไปที่ T การจัดการการทำลายเสร็จสิ้นทั้งหมดที่นี่ คลาส shared_ptr_base จัดการการนับการอ้างอิงโดยคำนึงถึงการคัดลอกและการมอบหมายเป็นต้นคลาส shared_ptr จัดการการใช้ typesafe ของโอเปอเรเตอร์โอเวอร์โหลด (->, * ฯลฯ )

ดังนั้นแม้ว่าคุณจะมี shared_ptr เป็นโมฆะภายใต้คุณกำลังจัดการตัวชี้ของประเภทที่คุณส่งเข้ามาใหม่ โปรดทราบว่าถ้าคุณแปลงตัวชี้ของคุณเป็นโมฆะ * ก่อนที่จะวางลงใน shared_ptr มันจะล้มเหลวในการรวบรวมบน checked_delete เพื่อให้คุณปลอดภัยจริง ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.