Einheitliches Mapping

Mit der Einführung von Hashmaps war es nun so weit:

Die Map APIs im GATE Projekt müssen umgestellt werden.

Und dieses Unterfangen war mühsamer als erwartet. Denn inzwischen bauen einige Applikationen darauf auf, die ebenso angepasst werden mussten.


Während C++ mit std::map und seit C++11 mit std::unordered_map jedes beliebige Key-Value Mapping problemlos bewältigen kann, hat man in C genau gar nichts im Standard, was einem hilft.

Natürlich könnte man hier unzählige Fremdbibliotheken einbinden, die alle zu einander inkompatibel sind und einen fetten Rattenschwanz an Abhängigkeiten nachziehen, oder man implementiert dieses Strukturen selbst und achtet auf möglichst viel Unabhängigkeit.

gate_map_t ist seit Anfang des GATE Projektes mit dabei und stellt eine generische Rot-Schwarz-Baum Implementierung bereit, die jedes beliebige Objekt (inklusive Konstruktor- und Destruktor-Funktion) aufnehmen kann.
Im C++ Layer nutzt das gate::Map<KEY, VALUE> Template die C Schnittstelle und macht sie ähnlich wie std::map nutzbar.
Schlüssel und Werte sind dabei direkt in die Baumknoten Objekte eingebaut und die C Iteratoren geben den Zugriff darauf über generische Pointer (void*) zurück, während C++ diese auf die korrekten Typ-Referenzen umwandelt.

Und dann kam die Sequence-Map hinzu, die im Endeffekt nur ein Array von Schlüssel-Wert Paaren ist und deren Auszeichnungsmerkmal das Erhalten der Reihenfolge des Hinzufügens ist.
Hier speichert ein gate_seqmap_t Objekt die Byte Offsets von Schlüssel und Wert in einem Eintrag und im Array wird stets ein ganzer Eintragsblock allokiert, wo die beiden Felder direkt hintereinander liegen.
Iteratoren nutzen dann die Offsets um aus einem Pointer dann zwei zu machen.

In den letzten Tagen kam nach der Implementierung einer Hashfunktion für die meisten GATE Typen eine Hashmap hinzu. Sie führt ein Array von verkürzten Hash-Codes in dem wiederum ein Array von Schlüssel-Wert Paaren liegen kann.

Und genau hier fiel mir mein Fehler auf: Alle Maps speichern Key/Value Paare, aber speichern sie immer anders.
Die Lösung war naheliegend:

Wir brauchen eine gemeinsame Datenstruktur, die alle Maps intern nutzen, damit Iteratoren und Enumeratoren immer die gleiche Schnittstelle zurückliefern können.

gate_mapping_t und gate::Mapping<KEY, VALUE>

Es ist eigentlich nur eine Art Header, der aus zwei Pointern besteht, nämlich einem void const* key; und einem void* value; und dieser ist der kleinste gemeinsame Nenner aller Formen von Map-Objekten. Die tatsächlichen Daten liegen in der Regel unmittelbar hinter den beiden Pointern, wenn gleich dieser Header auch zulassen würde, dass die Daten außerhalb der Maps liegen können.

classDiagram key_instance -- mapping_t value_instance -- mapping_t mapping_t <-- map_t : RB-Tree mapping_t <-- seqmap_t : Entry-Array mapping_t <-- hashmap_t : Hash-Buckets mapping_t : void const* key mapping_t : void* value map_t : -tree_node* root map_t : -compare_func key_comparer map_t : +add(key, value) map_t : +get(key) map_t : +remove(key) seqmap_t : -mapping_t** contents seqmap_t : -compare_func key_comparer seqmap_t : +add(key, value) seqmap_t : +get(key) seqmap_t : +remove(key) hashmap_t : -bucket_t** buckets hashmap_t : -compare_func key_comparer hashmap_t : -hash_func key_hash_generator hashmap_t : +add(key, value) hashmap_t : +get(key) hashmap_t : +remove(key)

Hilfreich ist das vor allem für Enumeratoren, diese stellen eine einheitliche Form des Auflistens von Containern zur Verfügung und bisher hat jede Map etwas anderes zurückgegeben.
Ab sofort liefen alle Maps einen C Enumerator von gate_mapping_t zurück und der C++ Wrapper castet das wiederum auf gate::Enumerator<gate::Mapping<KEY, VALUE> >

Nun kann man also endlich einheitlich mit allen Key/Value Containern arbeiten.

Zucker für den Debugger

In C++ im Debugging-Modus (NDEBUG oder GATE_DEBUG_MODE) hat nun das Mapping Template einen kleinen Zusatz spendiert bekommen hat, der in etwa so aussieht:

 1template<class KEY, class VALUE>
 2class Mapping
 3{
 4private:
 5#if defined(GATE_DEBUG_MODE)
 6  union
 7  {
 8    gate_mapping_t        impl;
 9    struct
10    {
11      KEY const* key_shadow;
12      VALUE const* value_shadow;
13    };
14  };
15#else
16  gate_mapping_t        impl;
17#endif

Durch den Einsatz von union wird im Template parallel zu den generischen void* Pointern auch noch eine typisierte Variante sichtbar. Und im Debugger kann man auf diese Weise den Inhalt der Datentypen inspizieren.

Das Schöne dabei ist, dass sich das Speicherlayout des Objektes dabei nicht ändert und man keine Inkompatibilitäten zwischen Debug und Release Variante schafft.

Fazit

Nun, solche “Überarbeitungen” sind mühsam, wenn man zahlreiche andere Codestellen anpassen muss, wo die vorherige Schnittstelle im Einsatz war. Doch es lohnt sich, wenn man nun viel generischer zwischen unterschiedlichen Containertypen hin und her wechseln kann.

Und dass das nun auch in purem C Code möglich ist, freut mich natürlich ganz besonders. Schließlich sind es genau diese Features aus C++, die man in C immer so schmerzlich vermisst.


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!

Meine Dokus über:
 
Externe Links zu: