Handle Handling

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

typedef void*  handlestore_t;

template<class T, bool HEAP> struct HandleAccessor;

/* HANDLE is on the Heap */
template<class T> struct HandleAccessor<T, true>
{
private:
    handlestore_t&	store;
public:
    HandleAccessor(handlestore_t& handle)
    : store(handle)
    { }

    void create(T const& src)
    {
        T* ptr = new T(src);  // create a new Object on Heap
        this->store = ptr;    // store IS the pointer to the object
    }
    void destroy()
    {
        T* ptr = static_cast<T*>(this->store);
        delete ptr;  // delete the heap object
        this->store = NULL;
    }
    T& get()
    {
        T* ptr = static_cast<T*>(this->store);
        return *ptr;  // return a pointer to the heap object
    }
};

/* HANDLE is embedded */
template<class T> struct HandleAccessor<T, false>
{
private:
    handlestore_t&	store;
public:
    HandleAccessor(handlestore_t& handle)
    : store(handle)
    { }
	
    void create(T const& src)
    {
        new (&this->store) T(src);  //create object IN the store
    }
    void destroy()
    {
        T* ptr = reinterpret_cast<T*>(&this->store);
        ptr->~T();  // explicitely call the destructor of T
        this->store = NULL;
    }

    T& get()
    {
        // get a T-Pointer from store address
        T* ptr = reinterpret_cast<T*>(&this->store);
        return *ptr;
    }
};

/* tiny helper trait-class: value is true, if sizeof(T) > sizeof(U) */
template<class T, class U> struct is_typesize_greater
{
    static const bool value = (sizeof(T) > sizeof(U));
};

template<class T> struct Handle 
: HandleAccessor<T, is_typesize_greater<T, handlestore_t>::value>
{
    typedef HandleAccessor<T, 
	  is_typesize_greater<T, handlestore_t>::value> baseclass_t;
	  
    Handle(handlestore_t& store)
    : baseclass_t(store)
    { }
};

// usage:

typedef int small_type_t;

typedef struct something
{
    int 	x;
    double y;
    void* 	c;
} big_type_t;

int main()
{
    handlestore_t store1, store2;

    Handle<small_type_t> h1(store1);
    Handle<big_type_t> h2(store2);

    h1.create(42);
    h2.create(big_type_t());

    small_type_t& r1 = h1.get();
    big_type_t& r2 = h2.get();

    h1.destroy();
    h2.destroy();
	
    return 0;
}

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 Kompilieraufwand ebenso.


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!