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.