[C++] Warum funktioniert std :: shared_ptr <void>?


Answers

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

  • ein Zeiger auf das zu verwaltende Objekt
  • ein Zeiger auf die Löschfunktion, die zum Zerstören verwendet wird.

Die Löschfunktion Ihres shared_ptr<Test> , wie Sie es aufgebaut haben, die normale für Test , die den Zeiger in Test* umwandelt und ihn delete .

Wenn Sie Ihren shared_ptr<Test> in den Vektor shared_ptr<void> drücken, werden beide kopiert, obwohl der erste in void* konvertiert wird.

Wenn also das Vektorelement zerstört wird, indem die letzte Referenz mitgenommen wird, übergibt es den Zeiger an einen Löscher, der es korrekt zerstört.

Es ist eigentlich ein wenig komplizierter als das, weil shared_ptr einen Deletor- Funktor und nicht nur eine Funktion verwenden kann, so dass möglicherweise sogar Daten pro Objekt gespeichert werden und nicht nur ein Funktionszeiger. Aber für diesen Fall gibt es keine solchen zusätzlichen Daten, es wäre ausreichend, nur einen Zeiger auf eine Instantiierung einer Schablonenfunktion zu speichern, mit einem Schablonenparameter, der den Typ erfasst, durch den der Zeiger gelöscht werden muss.

[*] logisch in dem Sinne, dass es Zugriff auf sie hat - sie dürfen nicht Mitglieder des shared_ptr selbst sein, sondern anstelle eines Verwaltungsknotens, auf den sie zeigt.

Question

Ich habe Code gefunden, der std :: shared_ptr verwendet, um eine beliebige Bereinigung beim Herunterfahren durchzuführen. Zuerst dachte ich, dass dieser Code nicht funktionieren könnte, 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, wie sie für G ++ implementiert sind, zu tun haben. Da diese Objekte den internen Zeiger zusammen mit dem Zähler umhüllen, behindert die std::shared_ptr<test> von std::shared_ptr<test> zu std::shared_ptr<void> den Aufruf des Destruktors wahrscheinlich nicht. Ist diese Annahme richtig?

Und natürlich die viel wichtigere Frage: Ist dies garantiert nach dem Standard zu funktionieren oder könnten weitere Änderungen an den Interna von std :: shared_ptr vorgenommen werden, brechen andere Implementierungen diesen Code tatsächlich?




Test* ist implizit in void* umwandelbar, daher ist shared_ptr<Test> implizit in shared_ptr<void> aus dem Speicher konvertierbar. Dies funktioniert, weil shared_ptr die Zerstörung zur Laufzeit steuert, nicht zur Kompilierzeit. Sie verwenden intern die Vererbung, um den entsprechenden Destruktor so aufzurufen, wie er zur Zeit der Zuweisung war.




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

Wenn dies geschieht, ist der Typ des Objekts Y bekannt, so dass der Deleter für dieses shared_ptr Objekt weiß, welcher Destruktor aufgerufen werden soll und diese Information nicht verloren ist, wenn der Zeiger in einem Vektor von shared_ptr<void> gespeichert ist.

Tatsächlich erfordern die Spezifikationen, dass für ein empfangendes shared_ptr<T> -Objekt, um ein shared_ptr<U> shared_ptr<T> -Objekt zu akzeptieren, es wahr sein muss, und U* muss implizit in ein T* konvertierbar sein, und dies ist sicherlich der Fall mit T=void weil irgendein Zeiger kann implizit in eine void* . Es wird nichts über den ungültigen Code gesagt, der ungültig ist. In der Tat schreiben die Spezifikationen vor, dass dies korrekt funktioniert.

Technisch IIRC a shared_ptr<T> enthält einen Zeiger auf ein verstecktes Objekt, das den Referenzzähler und einen Zeiger auf das tatsächliche Objekt enthält; Durch das Speichern des Deleters in dieser verborgenen Struktur ist es möglich, dieses scheinbar magische Feature zu verwenden, während shared_ptr<T> so groß wie ein regulärer Zeiger bleibt (Dereferenzierung des Zeigers erfordert jedoch eine doppelte Indirektion

shared_ptr -> hidden_refcounted_object -> real_object