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

왜 이것이 작동하는지에 대한 아이디어가 있습니다 .G ++에 구현 된 std :: shared_ptrs의 내부와 관련이 있습니다. 이러한 객체는 내부 포인터를 카운터와 함께 감싸기 때문에 캐스트 std::shared_ptr<test>를받는 사람 std::shared_ptr<void>이 소멸자의 호출을 방해하지 않을 수 있습니다. 이 가정이 맞습니까?

물론 훨씬 더 중요한 질문 : 이것은 표준에 따라 작동하거나 std :: shared_ptr의 내부 변경 사항을 추가로 변경할 수 있습니까? 다른 구현에서는 실제로이 코드를 깨뜨릴 수 있습니까?


2
대신에 무슨 일이 있었습니까?
궤도에서 가벼운 레이스

1
캐스트가 없습니다-shared_ptr <test>에서 shared_ptr <void> 로의 변환입니다.
Alan Stokes

참고 : 여기에 표준에 대한 기사의 링크 :: 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_ptr가 파괴 되면 저장된 함수를 호출하고을 호출합니다 deleter.

std :: function으로 단순화되고 진행되는 유형 삭제의 간단한 스케치는 모든 참조 횟수 및 기타 문제를 피할 수 있습니다.

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)

A는 때 shared_ptr다른에서 복사 (또는 기본 구성)되어 Deleter가 당신이를 구성 할 때 그래서, 주위에 전달되는 shared_ptr<T>A로부터 shared_ptr<U>호출 소멸자도 주위에 전달되는 사항에 대한 정보 deleter.


잘못 인쇄 된 것 같습니다 : my_shared. 문제를 해결했지만 아직 편집 할 권한이 없습니다.
Alexey Kukanov

@Alexey Kukanov, @Dennis Zickefoose : 편집 해 주셔서 감사합니다.
David Rodríguez-dribeas

2
@ user102008 'std :: function'은 필요하지 않지만 조금 더 유연하지만 (아마도 전혀 중요하지 않습니다) 'delete_deleter <T>'를 다음과 같이 저장하면 유형 삭제 작동 방식이 변경되지 않습니다 유형 지우기를 수행하는 함수 포인터 'void (void *)': T가 저장된 포인터 유형에서 사라졌습니다.
David Rodríguez-dribeas

1
이 동작은 C ++ 표준에 의해 보장됩니다. 내 클래스 중 하나에서 유형 지우기가 필요 std::shared_ptr<void>하며 특정 기본 클래스에서 상속받을 수 있도록 쓸모없는 래퍼 클래스를 선언하지 않아도됩니다.
Violet Giraffe

1
@AngelusMortis : 정확한 삭제 프로그램은의 유형에 속하지 않습니다 my_unique_ptr. 에 때 main템플릿 인스턴스화되고 double선택 권리 Deleter가 있지만, 이 유형에 포함되지 않습니다 my_unique_ptr및 개체에서 검색 할 수 없습니다. Deleter가의 유형입니다 삭제 함수가 수신 할 때 개체에서 my_unique_ptr(를 rvalue 참조로 말을)이 함수는 Deleter가 무엇인지 알지 못하지과 요구 않습니다.
David Rodríguez-dribeas

35

shared_ptr<T> 논리적으로 [*]에는 적어도 두 개의 관련 데이터 멤버가 있습니다.

  • 관리되고있는 객체의 포인터
  • 이를 삭제하는 데 사용될 삭제 기능에 대한 포인터

당신의 Deleter가 기능 shared_ptr<Test>당신이 그것을 구성하는 방식 주어는 대한 정상이다 Test포인터를 변환 Test*하고 delete그것을이야.

당신이 당신을 누르면 shared_ptr<Test>의 벡터로 shared_ptr<void>, 모두 첫 번째가 변환되어 있지만 그 중이 복사됩니다 void*.

따라서 벡터 요소가 마지막 참조를 사용하여 파괴되면 포인터를 올바르게 삭제하는 삭제기로 포인터를 전달합니다.

실제로 함수보다 삭제 기 functor를shared_ptr 사용할 수 있으므로 함수 포인터가 아니라 객체 별 데이터가 저장 될 수도 있기 때문에 실제로는 이것보다 조금 더 복잡 합니다. 그러나이 경우 추가 데이터가 없으면 포인터를 삭제 해야하는 유형을 캡처하는 템플릿 매개 변수를 사용하여 템플릿 함수의 인스턴스화에 대한 포인터를 저장하는 것으로 충분합니다.

논리적으로 액세스 할 수 있다는 점에서 [*]-shared_ptr 자체의 구성원이 아니라 해당 관리 노드 대신 해당 노드 일 수 있습니다.


2
삭제 기능 / functor가 다른 shared_ptr 인스턴스에 복사되었다는 언급으로 +1-다른 답변에서 누락 된 정보.
Alexey Kukanov

이것은 shared_ptrs를 사용할 때 가상 기본 소멸자가 필요하지 않다는 것을 의미합니까?
ronag

@ronag 예. 그러나 적어도 다른 가상 구성원이있는 경우 소멸자를 가상으로 만드는 것이 좋습니다. (실수로 잊어 버리는 고통은 가능한 이점보다 큽니다.)
Alan Stokes

예, 동의합니다. 흥미롭지 않은 재미있는. 유형 삭제가이 "기능"으로 간주되지 않았 음을 알았습니다.
ronag

2
@ronag : shared_ptr적절한 유형으로 직접 생성하거나를 사용하는 경우 가상 소멸자가 필요하지 않습니다 make_shared. 그러나, 여전히 그것은이 저장 될 때까지 포인터의 종류 건설에서 변경할 수있는 좋은 생각이다 shared_ptr: base *p = new derived; shared_ptr<base> sp(p);멀리로, shared_ptr객체가 우려 base되지 derived가상 소멸자를 필요로하므로,. 이 패턴은 예를 들어 팩토리 패턴과 공통 일 수 있습니다.
David Rodríguez-dribeas

10

유형 삭제를 사용하기 때문에 작동합니다.

기본적으로을 빌드 할 때 shared_ptr하나의 추가 인수 (원하는 경우 실제로 제공 할 수 있음)를 전달합니다. 이는 삭제 기능입니다.

이 기본 functor는에서 사용하는 유형에 대한 포인터를 인수로 허용 shared_ptr하므로 void여기에서 사용한 정적 유형에 적절하게 캐스팅 test하고이 객체에서 소멸자를 호출합니다.

충분히 진보 된 과학은 마술처럼 느껴지지 않습니까?


5

shared_ptr<T>(Y *p)실제로 생성자 는 객체에 대해 자동으로 생성 된 삭제 프로그램 인 shared_ptr<T>(Y *p, D d)where를 호출 하는 것 같습니다 d.

이런 일이 발생하면 객체의 유형을 알 수 Y있으므로이 shared_ptr객체의 삭제자는 호출 할 소멸자를 알고 포인터가 벡터에 저장된 경우이 정보는 손실되지 않습니다 shared_ptr<void>.

실제로 스펙이 들어 왔는지에 대한 것을 요구 shared_ptr<T>객체가 받아들이는 shared_ptr<U>객체를 그것이 진정한해야하며 U*A와 암시 적으로 변환해야 T*이 확실히의 경우 T=void어떤 포인터가로 변환 할 수 있기 때문에 void*암시. 유효하지 않은 삭제 도구에 대해서는 아무 것도 언급되지 않았으므로 실제로 사양이 올바르게 작동하도록 요구하고 있습니다.

기술적으로 IIRC a shared_ptr<T>에는 참조 카운터가 포함 된 숨겨진 개체에 대한 포인터와 실제 개체에 대한 포인터가 있습니다. 이 숨겨진 구조에 삭제기를 저장하면 shared_ptr<T>일반 포인터만큼 큰 크기를 유지하면서 마술 처럼 작동하는 것이 가능합니다 (그러나 포인터를 역 참조하려면 이중 간접 참조가 필요합니다)

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*는 메모리에서 암시 적으로 변환 가능 void*하므로 shared_ptr<Test>암시 적으로 shared_ptr<void>메모리에서 변환 가능합니다 . 때문에이 작품은 shared_ptr실행시 제어 파괴 설계, 컴파일 시간이 없다, 그들은 내부적으로는 할당 당시 같이 적절한 소멸자를 호출하는 상속을 사용합니다.


더 설명해 주시겠습니까? 비슷한 질문을 방금 게시했습니다. 도움이된다면 좋을 것입니다!
Bruce

3

사용자가 이해할 수있는 매우 간단한 shared_ptr 구현을 사용하여이 질문에 답할 것입니다 (2 년 후).

먼저, 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을 만들 때 다음에 어떤 일이 일어날 지에 필수적입니다.

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가 void이고 U가 "테스트"클래스 인 경우 위의 상황에 유의하십시오. T에 대한 포인터가 아닌 U에 대한 포인터와 함께 make_sp_counted_impl ()을 호출합니다. 파괴 관리는 모두 여기를 통해 수행됩니다. shared_ptr_base 클래스는 복사 및 할당 등과 관련된 참조 횟수를 관리합니다. shared_ptr 클래스 자체는 연산자 오버로드 (->, * 등)의 안전한 형식 사용을 관리합니다.

따라서 void_shared_ptr이 있지만 아래에 new로 전달한 유형의 포인터를 관리하고 있습니다. shared_ptr에 넣기 전에 포인터를 void *로 변환하면 checked_delete에서 컴파일되지 않으므로 실제로도 안전합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.