Kein shared_ptr?

Bisher gab es im GATE Projekt keine Äquivalent zum std::shared_ptr. Der Grund dafür war einfach: Alle C-Objekte implementieren ihr eigenes intrusive Reference-Counting und die C++ Objekte sind daher nur leichtgewichtige Wrapper.

Damit sind die meisten GATE C++ Objekte bereits eine typisierte Abart von std::shared_ptr.

Doch ab nun ist alles anders …


Eine der interessantesten Fähigkeiten von Smart-Pointern wie std::shared_ptr ist es Objekte ohne Referenzzählung durch eben eine solche zu “erweitern”. Genau genommen wird ein Zählerobjekt geschaffen, das einen Pointer zum Zielobjekt hat.

Wir sprechen hier somit vom non-intrusive reference counting.

Schnittstellentechnisch sind diese Konstrukte also eine Art von Hülle um die eigentlichen Objekte, die auf diese Weise neue Features hinzufügen.

Mit der neuen C-Objekt Klasse der gate_wrapper_t* existiert nun auch im GATE Framework eine Struktur, die beliebige Objekt “aufnehmen” und eine Speicherverwaltung für diese bereitstellen kann.

Und auf der C++ Seite übernimmt das Template gate::Wrapper<T> die “schönere” Repräsentation dieses Features.

make_shared nach C portieren

Die noch wichtigere Eigenschaft von std::shared_ptr ist jedoch die Fähigkeit, das Datenobjekt und die Referenzzählung in einen Speicherblock zu packen, und diese Aufgabe übernimmt für gewöhnlich die Funktion std::make_shared.

Wenn man mit dem “unschönen” Code

1std::shared_ptr<T> my_ptr(new T);

arbeitet, generiert man zwei Probleme:

  1. new T generiert ein Speicherfragment und der Konstruktur von std::shared_ptr erzeugt ein weiteres, die mit einem Pointer aneinander gekettet werden.
    Problem: Speicherfragmentierung
  2. new T könnte gut gehen, aber dem std::shared_ptr könnte der freie Speicher ausgehen. Es fliegt eine Exception und schon wäre das vorhergehende new T zu einem unauflösbaren Memory-Leak geworden.

make_shared Erzeugt aber einen übergroßen Puffer in shared_ptr und konstruiert eine T Instanz in diesen Puffer hinein. Damit liegen beide Objekte hinereinander in einem Speicherblock und reduzieren die Fragmentierung. Und egal an welcher Stelle eine Exception fliegt, es wird alles sauber aufgeräumt.

Das gleiche Konzept ist auch mit gate_wrapper_t möglich, doch nachdem wir hier tief in der C Welt stecken, bedarf es der Speicherung von Funktionspointern zu den entsprechenden Kon- und Destruktoren.

Der C-Teil kann nur flachen Speicher bereitstellen und diesen wieder freigeben. Daher muss sich der C++-Teil darum kümmern das finale Objekt auf den Speicher zu konstruieren. … nun gut, das geht beim Erstellen des Objektes in einem Schritt und findet alles auf der C++ Seite statt.

Doch die Löschung findet im C-Teil statt und somit muss der C++ Teil schon beim Erzeugen des Objektes eine Destruktor-Funktion miterstellen und diese dem C-Teil übergeben, damit sie dieser bei der finalen Löschung aufrufen kann.

Ein Vorteil dieser Lösung - meines Erachtens nach - ist, dass man nun C++ Objekte verwaltet durch viele Programmschichten (in C und anderen Sprachen) weiterreichen kann, um sie am Ende in einem anderen C++ Abschnitt weiterverarbeiten zu können.

Ich hatte am Anfang lange überlegt, ob man diese Hilfskonstrukt nicht als “Carrier” bezeichnen soll und offen gesagt erinnert die API eher an ein “Trägerobjekt”. Doch in der Anwendung und vor allem auf der C++ Seite wirkt die Schnittstelle (danke Templates) eben wie ein SmartPointer wo man ein Objekt über ein anderes aufgerufen wird … und dafür passt dann “Wrapper” besser.

Fazit

Es ist somit quasi das vierte Mal in meinem Leben, dass ich eine von std::share_ptr inspirierte SmartPointer Implementierung selbst schreibe.

Und dadurch, dass die GATE Lösung sich nach C abbilden und mit C++ interagieren kann, handelt es sich somit im die mächtigste dieser Implementierungen.

Gleichzeitig muss ich aber eingestehen, dass die Umwege über Funktionspointer in der Praxis ein klein bisschen Performance kosten werden. Denn bei einer reinen C++ Implementierung über Templates hatte der Compiler eine größere Chance auf Optimierungspotential.
C Funktionspointer reduzieren diese Möglichkeiten etwas, doch gerade in meinen Bereichen steht Kompatibilität stets über der Performance.

… und ganz nebenbei … wer weiß ob diese Unterschiede in der Praxis überhaupt messbar sind.

Wie auch immer … ein schöner Tag geht für mich zu Ende. Ich liebe es, sich mit solche Details zu befassen, die den meisten anderen gar nicht erst auffallen.


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!