Reference-Counter und Const-Correctness

Programmiersprachen, APIs, Bibliotheken, Open Source Engines, Debugging, Quellcode Fehler und alles was mit praktischer Programmierung zu tun hat.
Antworten
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

Hallo zusammen,

ich habe ein Basis-Interface, das ähnlich dem IUnknown-Interface Methoden zur Referenzzählung (AddRef(), Release()) und zur Abfrage weiterer Basis-Interfaces (QueryInterface()) anbietet. (Falls es wichtig sein sollte: letztgenannte Methode liegt eigentlich nicht in dem selben Interface, sondern in einem vom Basis-Interface abgeleiteten Interface. Das liegt daran, dass ich keinen Bock darauf habe, dass der Benutzer sonst wahllos in einem völlig verzykelten Graph von Interface zu Interface querien kann - siehe auch diese wunderschönen Eigenschaften von COM aka Reflexivität, Transitivität, Symmetrie).

Soweit, sogut. Jetzt hätte ich aber gerne Const-Correctness, und somit erhalte ich eine const-Version von QueryInterface(), das ich dann auch auf einem konstanten Objekt aufrufen kann und das mir dementsprechend einen const-Pointer auf das abgefragte Interface gibt. Intern muss dabei allerdings der Referenz-Zähler des abgefragten Interfaces hochgezählt werden, was auf einem const-Objekt nicht geht, es sei denn, ich markiere AddRef() als const. Das fühlt sich aber komisch an, denn diese Methode ändert ja schon klar dem Namen nach den Objektzustand. Andererseits will ich auch const-Objekte per Referenz-Zähler verwalten können, so gesehen fühlt es sich auch wieder richtig an - der Zähler in der Implemetierung wird dann eben mutable. Und so gesehen muss Release() dann auch const werden.

Das (also const overload für QueryInterface(), sowie AddRef()/Release() von vornherein const) würde ich jetzt insgesamt als durchaus sinnig ansehen. Ich frage mich nur, wieso ich das sonst noch nirgens gesehen habe. Ist es mir nur nie aufgefallen, oder hat dieser Ansatz Nachteile? Was meint ihr?

Vielen Dank für eure Inputs!
Helmut
Establishment
Beiträge: 237
Registriert: 11.07.2002, 15:49
Wohnort: Bonn
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von Helmut »

Da QueryInterface intern AddRef aufruft musst du, wenn du eine const Version von QueryInterface anbieten willst, auch AddRef const und den Refcounter mutable machen. Da dann in IUnknown aber alle Methoden const und alle Member mutable sind kannst du dir die const-correctness auch gleich sparen. Zumal es nicht sehr sinnvoll ist wenn man ein Objekt mit ner konstanten Referenz trotzdem freigeben kann.
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

Helmut hat geschrieben:Da QueryInterface intern AddRef aufruft musst du, wenn du eine const Version von QueryInterface anbieten willst, auch AddRef const und den Refcounter mutable machen
Genau, das sagte ich ja auch. Was sich aber mit der Beobachtung deckt, dass man auch konstante Objekte gerne per Refcount verwalten möchte. AddRef und Relase sind ja nichts anderes als die Ansagen des Users an das Objekt "Ich verwende ich auch" sowie "Ich benötige dich nicht mehr". Und das würde man ja auch gerne Const-Objekten sagen können.
Helmut hat geschrieben:Zumal es nicht sehr sinnvoll ist wenn man ein Objekt mit ner konstanten Referenz trotzdem freigeben kann.
Oh doch! Siehe mal folgenden Fall: Der Benutzer fordert zweimal das gleiche Interface an, einmal konstant und einmal veränderbar. Der Reference Counter beträgt nun zwei. Jetzt ruft er auf dem veränderbaren Interface Release auf, da er es nicht mehr benötigt, und der Counter sinkt auf eins. Wenn er nun auf dem konstanten Interface Release() aufruft, fällt der Counter auf Null und die implementierende Instanz wird freigegeben. Moral der Geschichte: Auch auf const-Interfaces sollte man AddRef() und Release() aufrufen können, und auch bei ihnen können Freigaben passieren.

Die Alternative wäre, const-Interfaces von der Referenz-Zählung auszuklammern. Dann könnten solche vom Benutzer unbemerkt ungültig werden, was defintiv nicht anstrebenswert ist.

Oder anders gesagt: Referenz-Zählung ist ja nichts anders als eine verzögerte Freigabe, und die kann durchaus auch dann beim Zugriff über das const-Interface eintreten.
Helmut hat geschrieben:Da dann in IUnknown aber alle Methoden const und alle Member mutable sind kannst du dir die const-correctness auch gleich sparen
Wieso das? Ist ein Interface, dessen Methoden alle const sind, ein schlechtes Interface?

Abgesehen davon hat ein Interface keine Member, sondern nur die implementierende Klasse. Diese hätte dann einen mutable Referenzzähler. Alle anderen Members wären nicht muteable.

Du hast mir aber trotzdem schon viel geholfen! Ich teile zwar nicht deine Meinung an dieser Stelle, aber ich musste dadurch nochmal darüber nachdenken. Und ich komme mehr und mehr zur Einsicht, dass mein Idee defintiv eine gute Idee ist.
Benutzeravatar
Krishty
Establishment
Beiträge: 8267
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von Krishty »

Ich denke, dass ihr da constheit von deinen Zeigern mit constheit des dahinterliegenden Objekts durcheinanderschmeißt (was COM ja so vormacht). Was möchtest du denn mit deiner Klasse gern imitieren: Einen Zeiger oder eine Referenz?

Einen Zeiger kann man const deklarieren ohne dass das Objekt, auf das verwiesen wird, const ist (und umgekehrt). Eine Referenz ist hingegen transparent und kennt nur constheit des Objekts, auf das sie verweist.

Dass du überhaupt AddRef() und Release() verwendest, impliziert mir, dass du auch Besitzrechte herumreichst (das zeigst du auch in deinem Beispiel). In diesem Fall solltest du Zeigereigenschaften imitieren. Dort kann man durchaus ein nicht-const-Objekt durch einen const-Zeiger freigeben – nur ist dieser Zeiger dann nicht Teil der Schnittstelle, auf die er verweist.

AddRef(), QueryInterface(), und Release() sind kein Teil deiner COM-Schnittstelle. Sie werden dir nur zur Verfügung gestellt, damit du deine Art von Referenz (automatisch, manuell, garbage collected) implementieren kannst und haben nichts mit der Implementierung zu tun, worauf deine Schnittstelle verweist; oder damit, was dein Objekt tut. Sie sind nur da weil ein Satz Methoden einfacher zu verwalten ist (und sich einfacher in andere Sprachen übersetzen lässt) als zwei getrennte Sätze für Schnittstelle und Lebenszeitverwaltung. Du darfst das Trio immer aufrufen – egal, ob du auf eine const-Schnittstelle verweist oder nicht. Steck die drei auf Seite deiner Zeiger-Implementierung und alle anderen Methoden stellst du als tatsächliche Schnittstelle zur Verfügung.

Das bedeutet aber dummerweise auch, dass AddRef(), QueryInterface(), und Release() vor dem Benutzer versteckt werden müssen, was C++ nicht erlaubt. Eine „perfekte“ COM-Zeiger-Klasse würde also nur auf eine gefälschte C++-Schnittstelle verweisen, die alle Methoden exklusive dem Trio für die Lebenszeit und Casting anbietet und an die tatsächliche COM-Schnittstelle weiterleitet. Und dann könnte man die Methoden auch direkt in Inspektoren und Mutatoren unterteilen und die Inspektoren const deklarieren.

… und damit habe ich einen weiteren Grund, meine eigenen Header zu schreiben. Und OOP beschissen zu finden.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

Krishty hat geschrieben:Ich denke, dass ihr da constheit von deinen Zeigern mit constheit des dahinterliegenden Objekts durcheinanderschmeißt (was COM ja so vormacht). Was möchtest du denn mit deiner Klasse gern imitieren: Einen Zeiger oder eine Referenz?
In der Tat, ich hatte stillschweigend die Zeigersemantik im Kopf, ohne mir explizit über diesen Punkt im Klaren gewesen zu sein. Das hätte ich dazu sagen müssen. Ich denke jedenfalls, dass die Zeigersemantik an dieser Stelle die richtige sein müsste (?).

Um mal meinen konkreten Anwendungsfall anzureißen:
  • Ich designe gerade eine API zum Ansteuern bestimmter Hardware. Die Implementierung liegt in einer DLL (das geht an dieser Stelle auch nur so), und wird von den Anwendungen dynamisch geladen.
  • Um die DLL zu verwenden, erhalten Anwendungsentwickler eine Header-Only API, die die Hardware-Funktionalitäten in Form einzelner Interfaces zur Verfügung stellt.
  • Instanzen der Hauptinterfaces werden durch exportierte Funktionen der DLL erstellt (ala FooBar* CreateFooBar()), deren Ownership dann beim User liegt - er muss sie also wieder freigeben. Momentan geht das über den Referenzzähler und Release().
  • Die Hauptinterfaces können wiederum mittels besagtem QueryInterface() Zugriff auf "Sub"-Interfaces gewähren. Rein technisch gesehen liegt deren Ownership beim jeweiligen Hauptinterface (in der Implementierung sind sie nämlich members der implementierenden Klasse des Hauptinterfaces), aber trotzdem möchte ich sie auch mit Referenzzähler versehen. Und zwar aus dem Grund, dass das Hauptinterface erst dann freigegeben wird, wenn keiner mehr die zuvor angeforderten Subinterfaces verwendet.
Es geht mir nicht darum, COM nachzubauen oder zu verwenden. Ich möchte einzig und allein Referenzzählung verwenden sowie die Möglichkeit, Interfaces anzufordern (aber deutlich eingeschränkter als in COM, wo man ja prinzipiell von jedem Interface zu jedem springen kann).

Krishty hat geschrieben:Einen Zeiger kann man const deklarieren ohne dass das Objekt, auf das verwiesen wird, const ist (und umgekehrt). Eine Referenz ist hingegen transparent und kennt nur constheit des Objekts, auf das sie verweist.

Dass du überhaupt AddRef() und Release() verwendest, impliziert mir, dass du auch Besitzrechte herumreichst (das zeigst du auch in deinem Beispiel). In diesem Fall solltest du Zeigereigenschaften imitieren. Dort kann man durchaus ein nicht-const-Objekt durch einen const-Zeiger freigeben (...)
Ja. Genau so dachte ich mir das.

Krishty hat geschrieben:(...) nur ist dieser Zeiger dann nicht Teil der Schnittstelle, auf die er verweist.
Guter Punkt, gar nicht darüber nachgedacht, aber ja. Das AddRef() sowie Release() dienen tatsächlich nur dem Management des Ownerships der jeweiligen Instanz, und gehören somit nicht wirklich in das betreffende Interface. Bei QueryInterface() könnte man sich schon streiten - das würde ich noch eher als dem Interface zugehörig ansehen.

Wenn ich deine Antworten richtig deute, bräuchte ich einen eigenen Smartpointer, der sich um Referenzzählung kümmert. Da diese Methoden dann in dem eigentlichen Interface fehlen, ist das sauberer getrennt und die const-Probleme sind weg. Die Frage ist nun: wie sieht denn jetzt eine gute Lösung aus?

Momentan habe keine eigene Smartpointer-Klasse - stattdessen dachte ich, dass Benutzer einfach den intrusive_ptr verwenden könnten und den kann ich ja prima über AddRef() und Release() an die Interfaces ankoppeln. Aber gut, das könnte man ja ändern, wenngleich ich es merkwürdig finde, einen eigenen SmartPointer schreiben zu müssen. Dummerweise bin ich da aber auch recht eingeschränkt, denn das Ganze geht wie gesagt über DLL-Grenzen hinweg und zwei verschiedene Heaps - also einfach SmartPointer-Objekte aus der DLL geben ist schon mal nicht. Das könnte nur Headerseitig gemacht werden.

So, wie es theoretisch schön wäre, ist jetzt schon mal klar geworden - danke dafür! Leider habe ich jetzt keinen Plan, wie man das jetzt auch in die Praxis umsetzen könnte. Ich habe das unterschwellige Gefühl, dass man das gar nicht schön machen kann, und am Ende tatsächlich bei dem COM-ähnlichen Mischmasch landet.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von dot »

Wieso nicht einfach std::shared_ptr verwenden, das Layout deiner Interfaces ist eigentlich eh sowieso auch nicht portabel. Wenn du Portabilität haben willst, müsstest du genaugenommen COM verwenden...hätte auch den Vorteil, dass deine API plötzlich allen möglichen Sprachen zugänglich wird... ;)
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

dot hat geschrieben:Wieso nicht einfach std::shared_ptr verwenden(...)
Das würde ich sehr gerne - allerdings sehe ich keine Möglichkeit, wie ich das anstellen sollte. std::shared_ptr ist ein Template und erledigt die Freigabe per delete, und das wird aufgrund der getrennten Heaps dann schiefgehen. Dann müsste ich schon zusätzlich Operator delete überladen - klappt auch nicht, weil dieser zwingend den Destruktor aufruft. Oder übersehe ich da gerade etwas? Und wie erledige ich dann das Management der Sub-Interfaces bzgl. des Hauptinterfaces?
dot hat geschrieben:(...)das Layout deiner Interfaces ist eigentlich eh sowieso auch nicht portabel. Wenn du Portabilität haben willst, müsstest du genaugenommen COM verwenden(...)
Naja, das Interface-Layout bzw. die VMT ist quasi portabel. Solange ich rein abstrakte Interfaces ohne Funktionsüberladungen habe, sind diese zumindest unter allen gängigen C++ Windows-Compilern kompatibel, was mir völlig genügt. Die Ursache hierfür ist natürlich die Existenz von COM. COM selber verwenden wollte ich jetzt auch nicht, davon habe ich zu wenig Ahnung, zu wenig Zeit, und ausserdem finde ich dieses wahllose Interface-Gehopse für den User gruselig. Die einzig völlig kompatible Variante wäre das Ausweichen auf ein c-Funktionsinterface in Verbindung mit einem Handle, aber da schreibe ich mich an Wrapper-Code ja blöde.
Benutzeravatar
Biolunar
Establishment
Beiträge: 154
Registriert: 27.06.2005, 17:42
Alter Benutzername: dLoB

Re: Reference-Counter und Const-Correctness

Beitrag von Biolunar »

FlorianB82 hat geschrieben:
dot hat geschrieben:Wieso nicht einfach std::shared_ptr verwenden(...)
Das würde ich sehr gerne - allerdings sehe ich keine Möglichkeit, wie ich das anstellen sollte. std::shared_ptr ist ein Template und erledigt die Freigabe per delete, und das wird aufgrund der getrennten Heaps dann schiefgehen. Dann müsste ich schon zusätzlich Operator delete überladen - klappt auch nicht, weil dieser zwingend den Destruktor aufruft. Oder übersehe ich da gerade etwas? Und wie erledige ich dann das Management der Sub-Interfaces bzgl. des Hauptinterfaces?
Jupp, du übersiehst etwas.Der C’tor von shared_ptr hat Haufenweise Überladungen, denen man auch einen custom deleter angeben kann. Diese deleter werden anstatt dem C++ delete aufgerufen, wenn das der ref count vom Zeiger 0 wird.
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

Biolunar hat geschrieben:Jupp, du übersiehst etwas.Der C’tor von shared_ptr hat Haufenweise Überladungen, denen man auch einen custom deleter angeben kann. Diese deleter werden anstatt dem C++ delete aufgerufen, wenn das der ref count vom Zeiger 0 wird.
Super, Dankeschön, das wußte ich noch nicht.

Allerdings: Nutzt mir das etwas? Das wäre auch wieder nur in meinem API-Header möglich, denn STL-Instanzen über DLL-Grenzen sind ebenfalls eine ungute Idee. Und dann lande ich doch schon dort, dass ich aus den in meinem Header definierten Custom-Deletern sowas wie AddRef() und Release() auf meinen Interfaces aufrufen müßte (denn ich möchte ja den internen Referenzzähler der Instanzen mitberücksichtigen, und nicht nur zählen, was der Benutzer von außen treibt). Und damit habe ich doch nicht viel gewonnen, oder?
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von dot »

Nunja, wenn es dir um Portabilität geht, dann sind STL Objekte natürlich absolut aus dem Interface rauszuhalten. Die ordentliche Lösung wäre dann imo allerdings COM oder ganz modern WinRT. Ansonsten würde ich generell mal die Sache mit dem Reference Counting in Frage stellen...
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

Was die Portabilität betrifft, weiß ich folgendes definitiv:
  • Andere Entwickler werden die Library mit unterschiedlichen Laufzeitbibliotheks-Versionen verwenden, da nutzt mir auch eine dynamisch gelinkte Laufzeitbibliothek nichts. Also muss ich von zwei verschiedenen Heaps ausgehen und kann keine STL-Instanzen durch die Gegend reichen.
  • Ich muss leider MSVC 2005 verwenden. Die anderen Entwickler können, ohne dass ich darüber Kontrolle hätte, diesen oder neuere MSVC-Versionen verwenden (Also auch Finger weg von RTTI und Exceptions).
  • Die erzeugten Anwendungen müssen auch auf XP lauffähig sein (WinRT geht also auch nicht).
Da bleibt ja fast nur noch COM (Bäh weil das bei mir dazu führen würde, dass man von jedem Interface zu jedem kommen würde - gruselig!), die selbstgestrickte Variante (die ich ja anstrebe, aber mir noch nicht sicher bin, ob das die beste Alternative ist), oder eben keine Referenzzählung (was mir schon fast ein wenig antik vorkommt).

Hach, das ist alles nicht optimal.
Benutzeravatar
dot
Establishment
Beiträge: 1734
Registriert: 06.03.2004, 18:10
Echter Name: Michael Kenzel
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von dot »

FlorianB82 hat geschrieben:Da bleibt ja fast nur noch COM (Bäh weil das bei mir dazu führen würde, dass man von jedem Interface zu jedem kommen würde - gruselig!)
Inwiefern kommt man in COM "von jedem Interface zu jedem" und in deiner selbstgestrickten Variante nicht? Du hast ja wohl auch so etwas wie QueryInterface()? Ich würde mir da eher noch die Frage stellen, wofür du überhaupt ein QueryInterface() hast, wenn du das nicht willst...

Ich seh bei COM halt zwei entscheidende Vorteile:
  • Portabilität tatsächlich sauber gelöst
  • Bekannt und bewährt anstatt die millionste Bastellösung
FlorianB82 hat geschrieben:[...]oder eben keine Referenzzählung (was mir schon fast ein wenig antik vorkommt).
Ich würde eher noch Referenzzählung als antik bezeichnen. Ist shared ownership denn wirklich notwendig? Alternativ könnte man einfach eine passende Smartpointer Implementierung als Header mitliefern...
Benutzeravatar
FlorianB82
Beiträge: 70
Registriert: 18.11.2010, 05:08
Wohnort: Darmstadt
Kontaktdaten:

Re: Reference-Counter und Const-Correctness

Beitrag von FlorianB82 »

Die von Dir genannten Vorteile von COM vermag ich ja nachzuvollziehen.

Aber was mich an COM für dieses Projekt stört, dass es viel Aufwand für etwas ist, was ich so in seiner Gänze gar nicht brauche (siehe hier). Ich brauche doch nur:
  • Auf ausgewählten (ein bis zwei!) Interfaces möchte ich abfragen, ob gewisse Funktionssätze vorhanden sind und diese dann in Form weiterer Interfaces anfordern können.
  • Ich möchte den geteilten Besitz meiner Objekte verwalten.
dot hat geschrieben:Inwiefern kommt man in COM "von jedem Interface zu jedem" und in deiner selbstgestrickten Variante nicht?
COM fordert für QueryInterface() Reflexivität, Transitivität, sowie Symmetrie. Und da QueryInterface() eine Methode von IUnknown ist, und IUnknown die Basisklasse jeden Interfaces ist, bedeutet das, dass ich mich per QueryInterface() durch einen großen ungerichteten Graph voller Zyklen bewege.

Wie gesagt, ich bilde hier die Fähigkeiten von bestimmter Hardware ab, die je nach Typ gewisse Dinge kann oder eben auch nicht. Von einem bestimmten Hardware Device kann ich dann beispielsweise ein Foo1 Interface anfordern, dass mir die Steuerung eines bestimmten Aspekts / Funktionssatzes erlaubt. Für einen anderen Satz gibt es dann ein Foo2 Interface. Laut den COM Anforderungen an QueryInterface() muss ich dann auch von Foo1 zu Foo2 per QueryInterface() direkt kommen. Das ist in dieser Situation nicht sinnig.

Und es wird noch schlimmer: Ich habe einen Satz von ungefähr 15 Interfaces. Einige davon sind Spezialisierungen anderer. Manche enthalten andere als Subinterfaces; haben also beispielsweise eine Methode getItems(index), die basierend auf einem Index eine von n Foo3 Instanzen zurückgibt. Unterm Strich gibt das einen riesigen Graphen, in dem der Nutzer herumspringen kann. Und das finde ich vom Design völligst unnötig und sinnlos komplex.

Wenn ich kein COM verwende, muss ich mich nicht diesen Anforderungen beugen. Dann hat das Device eine Methode zum Anfordern der Interfaces, und gut ist. Das ist doch viel besser, oder nicht?
dot hat geschrieben:Ich würde eher noch Referenzzählung als antik bezeichnen. Ist shared ownership denn wirklich notwendig? Alternativ könnte man einfach eine passende Smartpointer Implementierung als Header mitliefern...
Ich fürchte ja. Die Alternative wäre dem Nutzer zu sagen: Wenn das Hauptobjekt (das Device) freigeben wird, werden automatisch alle Subinterfaces ungültig. Vergisst er das, macht es Paff. Mit Shared Ownership wird die Freigabe des Devices so lange herausgezögert, bis er keines der Subinterfaces mehr verwendet (er auf allen diesen Release() aufgerufen hat). Was findest du besser?

Und wieso ist Referenzzählung antik?

Ich habe keine Probleme, eine passende Smartpointer-Implementierung als Header mitzuliefern. Allerdings: Trotzdem müssen wegen getrennten Heaps / DLL-Boundary die eigentlichen Referenzzähler/Freigabemechanismen über die Interfaces herausgeführt werden, damit sie die SmartPointer-Implementierung verwenden kann. Und da kann ich dann auch gleich std::shared_ptr oder std::intrusive_ptr daran ankoppeln, und brauche keinen eigenen SmartPointer-Typ.
Antworten