C++23: Vier neue assoziative Container

Insgesamt gibt es für C++ nun zwölf assoziative Container. In C++23 haben wir sie aus zwei Gründen: Speicherverbrauch und Performanz.​

vorlesen Druckansicht 15 Kommentare lesen

(Bild: rawf8/Shutterstock.com)

Lesezeit: 5 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Die vier assoziativen Container std::flat_map, std::flat_multimap, std::flat_set und std::flat_multiset in C++23 sind ein einfacher Ersatz fĂĽr die geordneten assoziativen Container std::map, std::multimap, std::set und std::multiset. In C++23 haben wir sie aus zwei GrĂĽnden: Speicherverbrauch und Performanz.

Modernes C++ – Rainer Grimm
Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Mit C++23 haben wir 12 assoziative Container. Zwölf? Richtig!

  • C++98: std::map, std::set, std::multimap und std::multiset
  • C++11: std::unordered_map, std::unordered_set, std::unordered_multimap und std::unordered_multiset
  • C++23: std::flat_map, std::flat_set, std::flat_multimap und std::flat_multiset

Hier tut Systematik dringend Not. Ich beginne mit den assoziativen Containern von C++98 und C++11.

Alle assoziativen Container haben gemeinsam, dass sie einen SchlĂĽssel mit einem Wert verknĂĽpfen. Daher kann man den Wert mithilfe des SchlĂĽssels erhalten. Die assoziativen Container von C++98 sind geordnete assoziative Container, die von C++11 ungeordnete assoziative Container.

Um die assoziativen Container zu klassifizieren, muss man drei einfache Fragen beantworten:

  • Sind die SchlĂĽssel sortiert?
  • Hat der SchlĂĽssel einen zugehörigen Wert?
  • Kann ein SchlĂĽssel mehr als einmal vorkommen?

Die folgende Tabelle mit 2Âł = 8 Zeilen beantwortet die drei Fragen. Ich beantworte eine vierte Frage in der Tabelle. Wie schnell ist die Zugriffszeit im durchschnittlichen Fall?

Gerne möchte ich mehr Informationen zu den assoziativen Containern geben, um ihre Performanz-Eigenschaften zu verstehen.

Die Schlüssel der geordneten assoziativen Container sind geordnet. Standardmäßig wird die kleinere Relation (<) verwendet und die Container werden in aufsteigender Reihenfolge sortiert.

Diese Ordnung hat interessante Konsequenzen.

  • Der SchlĂĽssel muss eine Ordnungsbeziehung unterstĂĽtzen.
  • Assoziative Container werden in der Regel als Red-Black Trees implementiert. Dies sind bilanzierte binäre Suchbäume.
  • Die Zugriffszeit auf den SchlĂĽssel und damit auch auf den Wert ist logarithmisch.

Der am häufigsten verwendete geordnete assoziative Container ist std::map:

std::map<int, std::string> int2String{ 
  {3, "three"}, {2, "two"}, {1, "one"},
  {5, "five"}, {6, "six"}, {4, "four"}, 
  {7, "seven"} };

Der bilanzierte binäre Suchbaum kann die folgende Struktur haben:

Der Kerngedanke der ungeordneten assoziativen Container ist, dass der Schlüssel mithilfe der Hash-Funktion auf den Bucket abgebildet wird. Der Wert wird einfach an den Schlüssel angehängt.

Die ungeordneten assoziativen Container speichern ihre Schlüssel in Buckets. In welchen Bucket der Schlüssel kommt, hängt von der Hash-Funktion ab, die den Schlüssel auf eine eindeutige Zahl abbildet. Diese Zahl wird durch die Anzahl der Buckets modulo geteilt. Wenn verschiedene Schlüssel demselben Bucket zugeordnet werden, spricht man von einer Kollision. Bei einer guten Hash-Funktion werden die Schlüssel gleichmäßig verteilt. Der Wert wird einfach mit dem Schlüssel verknüpft.

Die Verwendung der Hash-Funktion hat wichtige Konsequenzen fĂĽr den ungeordneten assoziativen Container.

  • Die Hash-Funktion fĂĽr den SchlĂĽssel muss verfĂĽgbar sein.
  • Die SchlĂĽssel mĂĽssen einen Gleichheitsvergleich unterstĂĽtzen, um mit Kollisionen umgehen zu können.
  • Da die AusfĂĽhrungszeit der Hash-Funktion eine Konstante ist, ist die Zugriffszeit auf die SchlĂĽssel eines ungeordneten assoziativen Containers konstant, wenn die SchlĂĽssel gleichmäßig verteilt sind.

Entsprechend zu std::map ist std::unordered_map der am häufigsten verwendete ungeordnete assoziative Container.

std::unordered_map<std::string, int> str2Int{ 
  {"Grimm",491634333356}, 
  {"Grimm-Jaud", 49160123335}, 
  {"Schmidt",4913333318},
  {"Huber",490001326} };

Die Grafik zeigt die Zuordnung der SchlĂĽssel zu ihrem Bucket mithilfe der Hash-Funktion.

Die vier assoziativen Container std::flat_map, std::flat_multimap, std::flat_set und std::flat_multiset in C++23 sind ein einfacher Ersatz fĂĽr die geordneten assoziativen Container std::map, std::multimap, std::set und std::multiset.

Die flach geordneten assoziativen Container in C++23 besitzen die gleiche Schnittstelle wie ihre Pendants in C++98.

Die flach geordneten assoziativen Container heißen Containeradapter, weil sie separate sequenzielle Container für ihre Schlüssel und Werte benötigen. Dieser sequenzielle Container muss einen Iterator mit wahlfreiem Zugriff unterstützen. Standardmäßig wird ein std::vector verwendet, aber auch ein std::array oder ein std::deque ist möglich.

Der folgende Codeschnipsel zeigt die Deklaration von std::flat_map und std::flat_set:

template<class Key, class T, 
        class Compare = less<Key>,
        class KeyContainer = vector<Key>, 
        class MappedContainer = vector<T>>
class flat_map;

template<class Key, 
         class Compare = less<Key>, 
         class KeyContainer = vector<Key>>
class flat_set;

Der Grund fĂĽr die flach geordneten assoziativen Container ist die Performanz.

Die flach geordneten assoziativen Container bieten eine andere Zeit- und Speicherkomplexität als die geordneten Container an. Sie benötigen weniger Speicherplatz und sind schneller zu lesen als ihre nicht flach geordneten Pendants. Sie bieten einen Iterator mit wahlfreiem Zugriff an.

Die nicht-flach geordneten assoziativen Container verbessern die Performanz beim Einfügen oder Löschen von einem Element. Außerdem garantieren sie, dass die Iteratoren nach dem Einfügen oder Löschen eines Elements gültig bleiben. Sie unterstützen einen bidirektionalen Iterator.

Bisher kann ich noch keine Zahlen zeigen, weil kein Compiler die flachen geordneten assoziativen Container unterstĂĽtzt. Ich werde meinen Performanz-Test "Mehr besondere Freunde mit std::map und std::unordered_map" aktualisieren, wenn std::flat_map verfĂĽgbar ist.

Ein std::mdspan ist eine nicht-besitzende mehrdimensionale Ansicht einer zusammenhängenden Folge von Objekten (non-owning multidimensional view of a contiguous sequence of objects). Diese zusammenhängende Folge von Objekten kann ein einfaches C-Array, ein Zeiger mit einer Größe, ein std::array, ein std::vector oder ein std::string sein. (rme)