Warum funktioniert std :: shared_ptr <void>?


129

Ich habe Code gefunden, der std :: shared_ptr verwendet, um beim Herunterfahren eine beliebige Bereinigung durchzuführen. Zuerst dachte ich, dieser Code könnte unmöglich funktionieren, aber dann habe ich Folgendes versucht:

#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;
}

Dieses Programm gibt die Ausgabe:

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

Ich habe einige Ideen, warum dies funktionieren könnte, die mit den Interna von std :: shared_ptrs zu tun haben, wie sie für G ++ implementiert sind. Da diese Objekte den internen Zeiger zusammen mit dem Zähler umschließen std::shared_ptr<test>, std::shared_ptr<void>behindert die Umwandlung von bis wahrscheinlich den Aufruf des Destruktors nicht. Ist diese Annahme richtig?

Und natürlich die viel wichtigere Frage: Funktioniert dies garantiert standardmäßig oder können weitere Änderungen an den Interna von std :: shared_ptr vorgenommen werden, wenn andere Implementierungen diesen Code tatsächlich beschädigen?


2
Was haben Sie stattdessen erwartet?
Leichtigkeitsrennen im Orbit

1
Es gibt dort keine Besetzung - es ist eine Konvertierung von shared_ptr <test> zu shared_ptr <void>.
Alan Stokes

Zu Ihrer Information : Hier ist der Link zu einem Artikel über std :: shared_ptr in MSDN: msdn.microsoft.com/en-us/library/bb982026.aspx. Dies ist die Dokumentation von GCC: gcc.gnu.org/onlinedocs/libstdc++/latest -doxygen / a00267.html
yasouser

Antworten:


98

Der Trick besteht darin, dass std::shared_ptrdas Löschen des Typs durchgeführt wird. Grundsätzlich wird beim Erstellen einer neuen Funktion shared_ptrintern eine deleterFunktion gespeichert (die dem Konstruktor als Argument übergeben werden kann, wenn sie nicht vorhanden ist, wird standardmäßig aufgerufen delete). Wenn das shared_ptrzerstört wird, ruft es diese gespeicherte Funktion auf und das ruft das auf deleter.

Eine einfache Skizze des Typlöschens, der mit std :: function vereinfacht wird und alle Referenzzählungen und andere Probleme vermeidet, finden Sie hier:

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)

Wenn a shared_ptrvon einem anderen kopiert (oder standardmäßig erstellt) wird, wird der Deleter herumgereicht, sodass beim Erstellen von a shared_ptr<T>aus einem shared_ptr<U>die Informationen darüber, welcher Destruktor aufgerufen werden soll, auch im übergeben werden deleter.


Es scheint einen Druckfehler zu geben : my_shared. Ich würde das beheben, habe aber noch kein Privileg zum Bearbeiten.
Alexey Kukanov

@Alexey Kukanov, @Dennis Zickefoose: Danke für die Bearbeitung, ich war weg und habe es nicht gesehen.
David Rodríguez - Dribeas

2
@ user102008 Sie benötigen 'std :: function' nicht, aber es ist etwas flexibler (spielt hier wahrscheinlich keine Rolle), aber das ändert nichts an der Funktionsweise des Löschens, wenn Sie 'delete_deleter <T>' als speichern Mit dem Funktionszeiger 'void (void *)' führen Sie dort die Typlöschung durch: T ist vom gespeicherten Zeigertyp verschwunden.
David Rodríguez - Dribeas

1
Dieses Verhalten wird durch den C ++ - Standard garantiert, oder? Ich muss in einer meiner Klassen einen Typ löschen und kann std::shared_ptr<void>vermeiden, dass eine nutzlose Wrapper-Klasse deklariert wird, damit ich sie von einer bestimmten Basisklasse erben kann.
Violette Giraffe

1
@AngelusMortis: Der genaue Deleter ist nicht Teil des Typs von my_unique_ptr. Wenn in mainder Vorlage mit doubledem richtigen Deleter instanziiert wird , ist dies jedoch nicht Teil des Typs von my_unique_ptrund kann nicht aus dem Objekt abgerufen werden. Der Typ des Löschers wird aus dem Objekt gelöscht , wenn eine Funktion eine my_unique_ptr(z. B. durch rWertreferenz) empfängt , weiß diese Funktion nicht und muss nicht wissen, was der Löscher ist.
David Rodríguez - Dribeas

35

shared_ptr<T> logischerweise hat [*] (mindestens) zwei relevante Datenelemente:

  • ein Zeiger auf das verwaltete Objekt
  • Ein Zeiger auf die Löschfunktion, mit der sie zerstört wird.

Die Deleter-Funktion von your shared_ptr<Test>ist in Anbetracht der Art und Weise, wie Sie sie erstellt haben, die normale für Test, die den Zeiger in Test*und deletes it konvertiert .

Wenn Sie Ihre shared_ptr<Test>in den Vektor von schieben shared_ptr<void>, werden beide kopiert, obwohl der erste in konvertiert wird void*.

Wenn das Vektorelement unter Verwendung der letzten Referenz zerstört wird, übergibt es den Zeiger an einen Löscher, der es korrekt zerstört.

Es ist eigentlich etwas komplizierter als das, weil shared_ptres einen Deleter- Funktor nehmen kann anstelle einer Funktion verwenden kann, sodass möglicherweise sogar pro Objekt Daten gespeichert werden und nicht nur ein Funktionszeiger. In diesem Fall gibt es jedoch keine solchen zusätzlichen Daten. Es würde ausreichen, nur einen Zeiger auf eine Instanziierung einer Vorlagenfunktion mit einem Vorlagenparameter zu speichern, der den Typ erfasst, über den der Zeiger gelöscht werden muss.

[*] logisch in dem Sinne, dass es Zugriff auf sie hat - sie sind möglicherweise keine Mitglieder des shared_ptr selbst, sondern anstelle eines Verwaltungsknotens, auf den es verweist.


2
+1 für die Erwähnung, dass die Löschfunktion / der Löschfunktion in andere shared_ptr-Instanzen kopiert wird - eine Information, die in anderen Antworten fehlt.
Alexey Kukanov

Bedeutet dies, dass bei Verwendung von shared_ptrs keine virtuellen Basis-Destruktoren benötigt werden?
Ronag

@ronag Ja. Ich würde jedoch weiterhin empfehlen, den Destruktor virtuell zu machen, zumindest wenn Sie andere virtuelle Mitglieder haben. (Der Schmerz des versehentlichen Vergessens überwiegt jeden möglichen Nutzen.)
Alan Stokes

Ja, ich würde zustimmen. Trotzdem interessant. Ich wusste, dass das Löschen von Typen dieses "Merkmal" einfach nicht berücksichtigt hatte.
Ronag

2
@ronag: Virtuelle Destruktoren sind nicht erforderlich, wenn Sie die shared_ptrdirekt mit dem entsprechenden Typ erstellen oder verwenden make_shared. Trotzdem ist es eine gute Idee, da sich der Typ des Zeigers von der Konstruktion bis zur Speicherung im shared_ptr: ändern kann. base *p = new derived; shared_ptr<base> sp(p);Soweit shared_ptres das Objekt basenicht betrifft derived, benötigen Sie einen virtuellen Destruktor. Dieses Muster kann beispielsweise bei Fabrikmustern üblich sein.
David Rodríguez - Dribeas

10

Es funktioniert, weil es Typlöschung verwendet.

Grundsätzlich wird beim Erstellen von a shared_ptrein zusätzliches Argument übergeben (das Sie auf Wunsch tatsächlich angeben können), nämlich der Deleter-Funktor.

Dieser Standardfunktor akzeptiert als Argument einen Zeiger auf den Typ, den Sie in verwenden shared_ptr, und voidwandelt ihn hier entsprechend dem von Ihnen verwendeten statischen Typ umtest hier, und ruft den destructor auf diesem Objekt.

Jede ausreichend fortgeschrittene Wissenschaft fühlt sich wie Magie an, nicht wahr?


5

Der Konstruktor shared_ptr<T>(Y *p)scheint tatsächlich shared_ptr<T>(Y *p, D d)wo anzurufend ein automatisch generierter Deleter für das Objekt ist.

In diesem Fall ist der Typ des Objekts Ybekannt, sodass der Löscher für dieses shared_ptrObjekt weiß, welcher Destruktor aufgerufen werden soll, und diese Informationen gehen nicht verloren, wenn der Zeiger in einem Vektor von gespeichert ist shared_ptr<void>.

In der Tat verlangen die Spezifikationen, dass ein empfangendes shared_ptr<T>Objekt, um ein Objekt zu akzeptieren shared_ptr<U>, wahr sein U*muss und implizit in a konvertierbar sein muss, T*und dies ist sicherlich der Fall, T=voidda jeder Zeiger void*implizit in ein konvertiert werden kann . Über den Deleter, der ungültig sein wird, wird nichts gesagt, so dass die Spezifikationen tatsächlich vorschreiben, dass dies korrekt funktioniert.

Technisch gesehen enthält IIRC a shared_ptr<T>einen Zeiger auf ein verstecktes Objekt, das den Referenzzähler enthält, und einen Zeiger auf das tatsächliche Objekt. Durch Speichern des Deleters in dieser verborgenen Struktur ist es möglich, dass diese scheinbar magische Funktion funktioniert, während sie immer noch shared_ptr<T>so groß wie ein normaler Zeiger bleibt (die Dereferenzierung des Zeigers erfordert jedoch eine doppelte Indirektion

shared_ptr -> hidden_refcounted_object -> real_object

3

Test*ist implizit konvertierbar in void*, ist daher shared_ptr<Test>implizit konvertierbar in shared_ptr<void>, aus dem Speicher. Dies funktioniert, da shared_ptrdie Zerstörung zur Laufzeit und nicht zur Kompilierungszeit gesteuert werden soll. Sie verwenden intern die Vererbung, um den entsprechenden Destruktor wie zur Zuweisungszeit aufzurufen.


Kannst du mehr erklären? Ich habe gerade eine ähnliche Frage gestellt. Es wäre großartig, wenn Sie helfen könnten!
Bruce

3

Ich werde diese Frage (2 Jahre später) mit einer sehr vereinfachten Implementierung von shared_ptr beantworten, die der Benutzer verstehen wird.

Zuerst gehe ich zu einigen Nebenklassen, shared_ptr_base, sp_counted_base sp_counted_impl und checked_deleter, von denen die letzte eine Vorlage ist.

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
};

Jetzt werde ich zwei "freie" Funktionen namens make_sp_counted_impl erstellen, die einen Zeiger auf eine neu erstellte zurückgeben.

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>() );
}

Ok, diese beiden Funktionen sind wichtig für das, was als nächstes passiert, wenn Sie einen shared_ptr über eine Vorlagenfunktion erstellen.

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->
};

Beachten Sie, was oben passiert, wenn T nichtig ist und U Ihre "Test" -Klasse ist. Es wird make_sp_counted_impl () mit einem Zeiger auf U und nicht mit einem Zeiger auf T aufgerufen. Die Verwaltung der Zerstörung erfolgt hier. Die Klasse shared_ptr_base verwaltet die Referenzzählung in Bezug auf Kopieren und Zuweisen usw. Die Klasse shared_ptr selbst verwaltet die typsichere Verwendung von Operatorüberladungen (->, * usw.).

Obwohl Sie ein shared_ptr zum Leeren haben, verwalten Sie darunter einen Zeiger des Typs, den Sie an new übergeben haben. Beachten Sie, dass wenn Sie Ihren Zeiger in eine Leere * konvertieren, bevor Sie ihn in shared_ptr einfügen, er nicht auf dem checked_delete kompiliert werden kann, sodass Sie auch dort tatsächlich sicher sind.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.