Selbstgebaute Schlüssel

std::map ist an sich ja schön und gut, aber man will ja oft nicht nur Strings und Integer als Schlüssel verwenden. Eigene Typen müssen allerdings bestimmte Anforderungen erfüllen, um hier Verwendung zu finden. Zum Glück bietet C++ mehrere Möglichkeiten, um diese Anforderungen in den unterschiedlichsten Situationen zu erfüllen.

Video

Quellcode

#include <iostream>
#include <map>
#include <string>

struct City
{
    std::string name;
    std::string country;
    
    City(std::string const &n, std::string const &c)
        : name { n }, country { c }
    {
    }
    
//     bool operator<(City const &other) const
//     {
//         return name < other.name || (name == other.name && country < other.country);
//     }
};

namespace std
{
    template <> struct less<City> {
        bool operator()(City const &c1, City const c2) 
        {
            return c1.name < c2.name || (c1.name == c2.name && c1.country < c2.country);
        }
    };
}

struct Compare
{
    bool operator()(City const &c1, City const &c2)
    {
        return c1.name > c2.name || (c1.name == c2.name && c1.country > c2.country);
    }
};

int main()
{
    std::map<City, unsigned int, Compare> inhabitants;
    
    inhabitants[City("London", "United Kingdom")] = 8308369;
    inhabitants[City("Berlin", "Deutschland")] = 3375222;
    inhabitants[City("Frankfurt/Main", "Deutschland")] = 687775;
    inhabitants[City("Berlin", "Conneticut, USA")] = 19590;
    
    for (auto c : inhabitants) {
        std::cerr << c.first.name << " (" << c.first.country << "): " << c.second << std::endl;
    }
}

Erklärung

Städte weltweit haben ja bekanntermaßen öfters mal gleiche Namen. Will man die also eindeutig kennzeichnen, dann braucht man noch weitere Informationen (bspw. das Land). Diese Informationen werden hier in der Klasse City gespeichert. Soll diese nun als Schlüssel für std::map verwendet werden, dann geht das erstmal daneben. Je nach Compiler (der GCC sticht hier leider sehr negativ hervor) wird man von einer ganzen Menge Fehlermeldungen erschlagen, die letztlich nur auf eins hinweisen: standardmäßig vergleicht std::map zwei Schlüssel mit dem Ausdruck k1 < k2. Wenn nun der eigene Typ keinen operator< unterstützt, dann ist erstmal Essig.

Damit ist auch schon die erste (und einfachste) Variante der Anpassung genannt: die Implementierung von operator< für den betreffenden Typ. Der auskommentierte Block in den Zeilen 15-18 enthält eine mögliche Variante. Was hier auffällt ist der zweite Teil des Ausdrucks in Zeile 17. Die naive Implementierung würde hier möglicherweise nur so aussehen: return name < other.name || country < other.country. Allerdings würde diese Variante die Anforderungen von std::map erfüllen. Für das Beispiel "Frankfurt, Deutschland" und "Berlin, USA" liefert diese Implementierung nämlich für beide Richtungen true: "Frankfurt, Deutschland" < "Berlin, USA" (aufgrund der Teilergebnisse false || true), umgekehrt allerdings auch (aufgrund von true || false, wobei der zweite Teil nach dem || nicht mehr ausgewertet wird, wenn vorher schon ein true auftaucht). Das ist nicht nur unlogisch, sondern auch verboten. Das Verhalten von std::map ist damit nicht mehr vorhersehbar. Das hier gewählte Muster ist für die Implementierung von mehrteiligen Schlüsseln immer wieder hilfreich: wenn ein Schlüssel aus den Teilen k1, k2 und k3 besteht, dann ist eine richtige Implementierung k1 < other.k1 || k1 == other.k1 && (k2 < other.k2 || k2 == other.k2 && k3 < other.k3). Natürlich ist das auf beliebig viele Teilschlüssel erweiterbar. Wichtig ist nur, dass bei Vergleich den n. Teilschlüssel sichergestellt ist, dass alle n-1 Teilschlüssel vorher gleich sind. 

In Situationen, wo eine Anpassung des betreffenden Typs mit einer Implementierung von operator< nicht möglich ist (bspw. bei Typen aus Libraries), bietet std::map über die Verwendung des Templates std::less zum Vergleich eine bequeme Möglichkeit, die Sortierung extern anzupassen. Dazu muss das betreffende Template für den entsprechenden Typ spezialisiert werden (Zeile 21-29). std::less ist ein Funktionstyp, dessen operator() zwei Parameter nimmt und true zurückliefert, wenn der erste kleiner, als der zweite ist. Für die Implementierung des operator() gelten natürlich die gleichen Regeln, wie für operator<.

Die beiden Varianten mit operator< und std::less wirken global: jede std::map (und einige weitere Datenstrukturen) wird ihre Sortierung entsprechend vornehmen. Will man hingegen nur die Sortierung einer einzelnen Map ändern, so kann man den dritten Templateparameter in der Variablendeklaration anpassen (wie in Zeile 41 zu sehen). Der zu übergebende Typ muss wie std::less ein Funktionstyp sein, der einen passenden operator() mit zwei Parametern implementiert. Im Beispiel ist das die Struktur Compare (Zeile 31-37). Der Typ muss mit einem Standardkonstruktur initialisierbar sein (Ausnahme: man übergibt das betreffende Vergleichsobjekt als Konstruktorparameter an die std::map. Braucht man aber eher selten.) Im Beispiel hier wird die Sortierung der std::map umgedreht.