Verständnisproblem shared_ptr moven
-
@hustbaer sagte in Verständnisproblem shared_ptr moven:
Vor Verlassen von main ist der Ref-Count des vom
shared_ptr
verwalteten Counter-Objekts 2 (dershared_ptr
selbst hat keinen Ref-Count!). Und die Variableptr
inmain
ist unverändert, du hast ja beim Aufruf vontestFunction
keinen Move erlaubt.D.h. es wird tatsächlich nur dieses Pärchen aus T* und Ref-Count gemoved ? D.h. nach dem Move in der Funktion testFunction ist "ptr" irgendwas zwischen leer und kaputt? Und das Pärchen ist weitergewandert in den Container, wodurch sich der Referencecounter selber nicht verändert hat.
void testFunction( std::shared_ptr<int> ptr ) { // Ref-Count = 2 SomeContainer.push_back( std::move( ptr ) ); // ptr "tot" / "leer" / "nullptr" } int main() { auto ptr = std::make_shared<int> ( 1 ); // Ref-Count = 1 testFunction( ptr ); // Ref-Count = 2 ( ptr selbst und die Kopie im Container ) }
-
D.h. es wird tatsächlich nur dieses Pärchen aus T* und Ref-Count gemoved ?
Es wird das Pärchen
T*
undUnspecifiedTypeOfSharedCountObject*
gemoved. Also ein Zeiger auf das "shared count" aka. "control block" Objekt, nicht das Objekt selbst. Wobei das ein Implementierungsdetail ist.shared_ptr
muss nicht zwingend ein "shared count" Objekt verwenden (aber alle Implementierungen die ich kenne machen es so).D.h. nach dem Move in der Funktion testFunction ist "ptr" irgendwas zwischen leer und kaputt?
Nach dem Move in
testFunction
ist derenptr
garantiert leer (vollständig gültiger Zustand). Weil alles andere unpraktisch wäre/für Überraschungen sorgen könnte. Siehe https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr- Move-constructs a shared_ptr from r. After the construction, *this contains a copy of the previous state of r, r is empty and its stored pointer is null. (...)
Und das Pärchen ist weitergewandert in den Container, wodurch sich der Referencecounter selber nicht verändert hat.
Rüschtüg.
-
Ok. Vielen Dank für die Aufklärung @hustbaer . Jetzt hab ichs auch geschnallt
Mir hat irgendwie die Info gefehlt, dass dort der Ref-Count auch als Zeiger liegt.... was ja auch zwangsläufig so sein muss, damit der ganze Quatsch funktioniert
-
@It0101 sagte in Verständnisproblem shared_ptr moven:
Mir hat irgendwie die Info gefehlt, dass dort der Ref-Count auch als Zeiger liegt.... was ja auch zwangsläufig so sein muss, damit der ganze Quatsch funktioniert
Nicht zwangsläufig. Man kann den Zähler auch zusammen mit dem gehaltenen Objekt speichern, dann käme man über einen fixen Offset vom Objektpointer dran. Implementierungen machen das sogar als Optimierung, wenn man
make_shared
verwendet, oderboost::intrusive_ptr
für alle damit verwalteten Objekte.Da ein
shared_ptr
allerdings beliebige Zeiger in Besitz nehmen kann und es auchweak_ptr
gibt, muss er natürlich auch einen separaten Zeiger auf einen eigenständigen Kontrollblock mit Zähler unterstützen.
-
@Finnegan sagte in Verständnisproblem shared_ptr moven:
Man kann den Zähler auch zusammen mit dem gehaltenen Objekt speichern, dann käme man über einen fixen Offset vom Objektpointer dran.
Nope, nicht bei
shared_ptr
.shared_ptr
erlaubt nämlich einen anderenshared_ptr
zu erstellen der auf was ganz anders zeigt, aber das selbe Objekt am Leben hält wie der originaleshared_ptr
. Das kann man z.B. schön verwenden wenn man einenshared_ptr
auf ein Member braucht.Das selbe Problem gibt es bei Basisklassen, denn nicht jede Basisklasse hat Offset 0.
Daher werden bei Intrusive Reference Counting auch oft allgemeine Basisklassen ala
IUnknown
verwendet, die dann virtuelle Funktionen haben um den Reference Count zu verwalten.
-
@hustbaer sagte in Verständnisproblem shared_ptr moven:
Das kann man z.B. schön verwenden wenn man einen
shared_ptr
auf ein Member braucht.Uhh, da ist was dran. Noch nicht wirklich gebraucht sowas, zeigt aber mal wieder schön wie viel letzendlich doch zu solchen oberflächlich relativ simplen Utility-Klassen dazu gehört.
Das selbe Problem gibt es bei Basisklassen, denn nicht jede Basisklasse hat Offset 0.
Ja, jetzt wo du das erwähnst muss ich auch an den
vptr
bei polymorphen Klassen denken, der es schwer macht zu bestimmen wo das Objekt intern im Speicher tatsächlich anfängt. Da könnte aber auch ein einziger Zeiger auf einstruct counted { int count; T object; }
abhelfen (für den Fall dass manmake_shared
verwendet hat), der dann inshared_ptr::get
einen Zeiger aufobject
zurückgibt. Für die Member Shared Pointer ist das natürlich keine Lösung.Aber wie ich schon sagte, das muss in einer Impelementierung nicht in jedem Fall (hier: für jedes
T
und für jede Art wie das Objekt erzeugt wurde,new
, selbst verwalteter Speicher mit Custom Deleter odermake_shared
) so sein, dershared_ptr
muss sowas aber unterstützen, z.B. mit einem oft ungenutzten zweiten Pointer. Das wären aber alles interne Optimierungen.
-
@Finnegan sagte in Verständnisproblem shared_ptr moven:
Da könnte aber auch ein einziger Zeiger auf ein struct counted { int count; T object; } abhelfen (für den Fall dass man make_shared verwendet hat), der dann in shared_ptr::get einen Zeiger auf object zurückgibt. Für die Member Shared Pointer ist das natürlich keine Lösung.
Das geht auch nur wenn du einen Zeiger auf den Most Derived Type hast oder zufällig der Offset Null ist. D.h. damit das mit Basisklassen in beliebigen Klassenhierarchien funktioniert bräuchtest du zusätzlich zum Zeiger auf das Struct noch einen Offset.
Theoretisch könnte man dann natürlich sagen man unterstützt z.B. max. 16 oder 32 Bit Offsets, damit das Objekt kleiner wird. Das würde vermutlich über 99.9% aller Fälle abdecken, der Teil wäre also nicht das Problem. Nur dass das Objekt dadurch nicht kleiner wird. Das Alignment eines Zeigers ist typischerweise gleich der Grösse, d.h. sogar ein Struct mit einem Zeiger + bloss einem
char
zusätzlich ist schon 2x so gross wie nur der Zeiger.Beim Speicherzugriff spart man sich auch nix. Was Speicherbandbreite und Cache-Auslastung angeht kostet es genau gleich viel einen
char
zu laden wie einen Zeiger von einer Adresse mit passendem Alignment zu laden.Und dann kann man gleich zwei Pointer speichern. Weil der Code dadurch viel einfacher wird. Es gibt dann direkt in
shared_ptr
keine Spezialfälle diesbezüglich mehr: du hast immer einenT*
auf das Objekt und einenC*
auf den Control-Block. Die einzige Fallunterscheidung die dann bleibt ist beim Zerstören desT
zu prüfen ob der Speicher für dasT
auch freigegeben werden soll (nein wennmake_shared
verwendet wurde und der Speicher für Objekt und Control-Block in eine Allocation zusammengefasst wurden, ansonsten ja).
Der einzige mir bekannte Ausweg ist wirklich in die Klassenhierarchie einzugreifen und die Information wie man auf den Counter zugreifen kann mit im Objekt abzuspeicher - und zwar so dass man von jeder Basisklasse aus zugreifen kann. In C++ verwendet man dazu gerne virtuelle Funktionen, da ein Objekt mit virtuellen Funktionen sowieso schon einen VTable Pointer pro Basisklassen-Subobjekt (mit virtuellen Funktionen) braucht (+1 für den most derived type natürlich). Ein paar mehr virtuelle Funktionen ala
AddRef
undRelease
machen dann keinen Unterschied mehr im Speicherverbrauch pro Objekt.Weitere Nebeneffekte von Intrusive Reference Counting (können je nach Situation Vorteile aber auch Nachteile sein):
- Du kannst auch ohne ein
xxx_ptr
Objekt zu erzeugen den Reference-Count ändern. - Du kannst aus jedem Raw-Pointer ohne weitere Hilfe einen ownership-sharing
xxx_ptr
Smart-Pointer machen.
Ich habe daher in bestimmten Situationen, selten, aber über die Jahre doch immer wieder mal,
boost::intrusive_ptr
verwendet. (Um eigene Objekte zu verwalten, also nicht nur für Sachen wie COM Objekte woboost::intrusive_ptr
ziemlich klar "die" Lösung ist.)
- Du kannst auch ohne ein
-
Dieser Beitrag wurde gelöscht!
-
@hustbaer sagte in Verständnisproblem shared_ptr moven:
@Finnegan sagte in Verständnisproblem shared_ptr moven:
Da könnte aber auch ein einziger Zeiger auf ein struct counted { int count; T object; } abhelfen (für den Fall dass man make_shared verwendet hat), der dann in shared_ptr::get einen Zeiger auf object zurückgibt. Für die Member Shared Pointer ist das natürlich keine Lösung.
Das geht auch nur wenn du einen Zeiger auf den Most Derived Type hast oder zufällig der Offset Null ist. D.h. damit das mit Basisklassen in beliebigen Klassenhierarchien funktioniert bräuchtest du zusätzlich zum Zeiger auf das Struct noch einen Offset.
Eigentlich will ich hier die ganze Zeit lediglich auf die
make_shared
-Optimierung hinaus. Ich hab nicht beauptet, dass man jeden Zeiger, den man im Konstruktor übergeben bekommt so managen kann. Beimake_shared
muss das konstruierte Objekt ja genau dem Typen desshared_ptr<T>
entsprechen.... oder habe ich da was übersehen und man kann mitmake_shared
einenshared_ptr<Base>
auf einDerived
-Objekt erstellen?Ich sehe in diesem Fall mit dem
struct counted
kein Problem. Bei einemshared_ptr::reset(pointer_to_derived)
wechselt die Implementierung dann eben wieder auf einen spearaten Kontrollblock.[...]
Beim Speicherzugriff spart man sich auch nix. Was Speicherbandbreite und Cache-Auslastung angeht kostet es genau gleich viel einenchar
zu laden wie einen Zeiger von einer Adresse mit passendem Alignment zu laden.Mir geht es hier auch vornehmlich darum einen separaten Speicherbereich für den Kontrollblock zu vermeiden und in Fällen wie z.B.
p = shared_ptr<T>(other); p->data = 42;
noch eine weitere Cache Line für den Kontrollblock involviert sein muss. Ich denke aber dieser Aspekt ist wohl unstrittig (?).Und dann kann man gleich zwei Pointer speichern. Weil der Code dadurch viel einfacher wird. Es gibt dann direkt in
shared_ptr
keine Spezialfälle diesbezüglich mehr: du hast immer einenT*
auf das Objekt und einenC*
auf den Control-Block. Die einzige Fallunterscheidung die dann bleibt ist beim Zerstören desT
zu prüfen ob der Speicher für dasT
auch freigegeben werden soll (nein wennmake_shared
verwendet wurde und der Speicher für Objekt und Control-Block in eine Allocation zusammengefasst wurden, ansonsten ja).Ja, das sehe ich ein, dass der Code mit zwei Pointern simpler wird, auch wenn der zweite im Speziallfall eben "direkt neben" den anderen zeigt. Da gibts von mir auch keinen Widerspruch, dass das wahrscheinlich bei den meisten
shared_ptr
so implemetiert ist. Hab mich ursprünglich an dem Der Quatsch funktioniertshared_ptr
muss zwei Pointer haben aufgehängt. Das obige Programm bekommt man nämlich auch mit nur einem Pointer ans "funkioneren"Der einzige mir bekannte Ausweg ist wirklich in die Klassenhierarchie einzugreifen [...]
Ich habe daher in bestimmten Situationen, selten, aber über die Jahre doch immer wieder mal,boost::intrusive_ptr
verwendet. (Um eigene Objekte zu verwalten, also nicht nur für Sachen wie COM Objekte woboost::intrusive_ptr
ziemlich klar "die" Lösung ist.)Ja, wenn ich wirklich mal die Ehre habe, eine richtige (polymorphe) OOP-Klassenhierarchie von Grund auf zu entwerfen, dann verwende ich auch sowas wie
boost::intrusive_ptr
- ansonsten ebenmake_shared
wenn möglich. Schade, dass es da keine wirklich gute "fire and forget"-Lösung gibt, die man ohne nachzudenken immer verwenden kann. Schliesslich wollen wir ja auch keinstd::pair
mit eingebautem Referenzzähler habenBezüglich COM: Das ist schon was länger her, aber gab es da nicht
Microsoft::WRL::ComPtr
? Den hab ich zumindest verwendet als ich as letzte mal mit COM zu tun hatte.
-
@Finnegan sagte in Verständnisproblem shared_ptr moven:
Eigentlich will ich hier die ganze Zeit lediglich auf die make_shared-Optimierung hinaus. Ich hab nicht beauptet, dass man jeden Zeiger, den man im Konstruktor übergeben bekommt so managen kann. Bei make_shared muss das konstruierte Objekt ja genau dem Typen des shared_ptr<T> entsprechen.... oder habe ich da was übersehen und man kann mit make_shared einen shared_ptr<Base> auf ein Derived-Objekt erstellen?
Das was von
make_shared<MostDerivedType>()
zurückkommt ist immer einMostDerivedType
, ja. Aber du kannst den Zeiger dann sofort in einen Basisklassen-Zeiger konvertieren:std::shared_ptr<Base> p = std::make_shared<Derived>(); // OK
Davon abgesehen ist der Typ von
std::shared_ptr<Derived>
immer identisch, egal wie du ihn initialisiert hast und ob er aufDerived
oder aufMoreDerived
zeigt. D.h.std::shared_ptr<Derived>
kann niemals nicht mit nur einem Zeiger (Membervariable) auskommen. Man könnte die zweite Membervariable in manchen Fällen NULL/0 lassen, ja. Aber nur wenn man unnötige Fallunterscheidungen einbaut. Und wieso sollte man das, wenn man die Membervariable sowieso mitschleppen muss.D.h.: So wie
std::shared_ptr
spezifiziert ist muss er immer mindestens zwei Membervariablen haben.
(EDIT: OK, das stimmt natürlich so nicht. Trivial kann man natürlich einpair<T*, C*>
nehmen, das wäre dann nur ein Member. Der Knackpunkt ist dass man mit bloss einemT*
/C*
/X*
als Member nicht auskommt.)