Handle Handling
« | 15 Nov 2018 | »Hmm … da gibt es so ein Thema, wo ich mit mir selbst lange diskutieren könnte und nie am Ende ankäme …
Und deshalb gibt es im GATE Projekt eine Entscheidung:
Verweise auf System-Ressourcen (unter Windows gerne HANDLE genannt) wandern in eine eigene Struktur, die dann darüber entscheiden darf, ob STACK oder HEAP der richtige Platz ist.
Wenn wir viel mit dem Betriebssystem reden, erhalten wird eine Vielzahl an unterschiedlichen Datentypen, die so wie Banknoten unseren Anspruch auf irgend etwas im System repräsentieren.
Und das ist ärgerlich, denn “unser eigenes” Plattform-unabhängiges
Objekt würde dann auf jeder Platform anders aussehen.
Schlimmer: wir müssten auch die Platform-Header in unsere Header aufnehmen.
Wer so dreist ist, <windows.h>
in seinen Header reinzuschreiben, wird zum
Duell mit dem Degen herausgefordert!
Denn so laden wir nämlich tausende Funktionsdeklarationen, die wir gar nicht
brauchen, in “unseren” Headern, die vor allem das Kompilieren stark
verlangsamen.
Das andere Extrem ist mit PIMPL
verwandt und bedient sich eines void*
Pointers. Die Systemressource wird per
malloc
oder new
im Speicher allokiert und der Pointer verweist darauf.
… auch nicht gut, denn besonders wenn der Ressourcen-Typ selbst nur ein Pointer ist, pflastern wir den Heap mit kleinen Hilfsverweisen völlig zu.
Nur ein Sith kennt nichts als Extreme.
Und damit hatte Obi-Wan Kenobi recht.
Die Lösung liegt dazwischen:
Stellt euch vor, wir haben einen Datentypen, der im Falle kleiner Objekte diese in sich selbst speichert, bei größeren aber zum Pointer mutiert und die Daten am Heap allokiert, so haben wir eine Komponente, die sich dynamisch in die nächste-beste Lösung verwandelt.
In C++ wäre wieder alles so leicht:
Ein Template ->
zwei Spezialisierungen ->
perfekter Code
1typedef void* handlestore_t; 2 3template<class T, bool HEAP> struct HandleAccessor; 4 5/* HANDLE is on the Heap */ 6template<class T> struct HandleAccessor<T, true> 7{ 8private: 9 handlestore_t& store; 10public: 11 HandleAccessor(handlestore_t& handle) 12 : store(handle) 13 { } 14 15 void create(T const& src) 16 { 17 T* ptr = new T(src); // create a new Object on Heap 18 this->store = ptr; // store IS the pointer to the object 19 } 20 void destroy() 21 { 22 T* ptr = static_cast<T*>(this->store); 23 delete ptr; // delete the heap object 24 this->store = NULL; 25 } 26 T& get() 27 { 28 T* ptr = static_cast<T*>(this->store); 29 return *ptr; // return a pointer to the heap object 30 } 31}; 32 33/* HANDLE is embedded */ 34template<class T> struct HandleAccessor<T, false> 35{ 36private: 37 handlestore_t& store; 38public: 39 HandleAccessor(handlestore_t& handle) 40 : store(handle) 41 { } 42 43 void create(T const& src) 44 { 45 new (&this->store) T(src); //create object IN the store 46 } 47 void destroy() 48 { 49 T* ptr = reinterpret_cast<T*>(&this->store); 50 ptr->~T(); // explicitely call the destructor of T 51 this->store = NULL; 52 } 53 54 T& get() 55 { 56 // get a T-Pointer from store address 57 T* ptr = reinterpret_cast<T*>(&this->store); 58 return *ptr; 59 } 60}; 61 62/* tiny helper trait-class: value is true, if sizeof(T) > sizeof(U) */ 63template<class T, class U> struct is_typesize_greater 64{ 65 static const bool value = (sizeof(T) > sizeof(U)); 66}; 67 68template<class T> struct Handle 69: HandleAccessor<T, is_typesize_greater<T, handlestore_t>::value> 70{ 71 typedef HandleAccessor<T, 72 is_typesize_greater<T, handlestore_t>::value> baseclass_t; 73 74 Handle(handlestore_t& store) 75 : baseclass_t(store) 76 { } 77}; 78 79// usage: 80 81typedef int small_type_t; 82 83typedef struct something 84{ 85 int x; 86 double y; 87 void* c; 88} big_type_t; 89 90int main() 91{ 92 handlestore_t store1, store2; 93 94 Handle<small_type_t> h1(store1); 95 Handle<big_type_t> h2(store2); 96 97 h1.create(42); 98 h2.create(big_type_t()); 99 100 small_type_t& r1 = h1.get(); 101 big_type_t& r2 = h2.get(); 102 103 h1.destroy(); 104 h2.destroy(); 105 106 return 0; 107}
Und in C?
Dafür haben wir ja das GATE Projekt … wir brauchen zwar einen weiteren
Pointer im Store, weil C leider keine templates
hat,
aber ansonsten ist es ähnlich.
Jetzt kann es uns egal sein, ob ein Thread, Mutex oder eine Semaphore nur ein
Pointer oder eine fette Struktur ist. “Unser Handle” sucht sich die richtige
Art der Speicherung automatisch aus.
In unseren Headern finden wir nur handlestore_t
, und die Implementierungen
ziehen sich den eigentlichen Datensatz aus dem Handle-Store heraus.
Der Coding-Aufwand hält sich in Grenzen und der Speicher- und Übersetzungsaufwand ebenso.