std::basic_string anpassen

Mittels Character Traits isoliert sich std::basic_string von den kleinen, fiesen Details der Buchstaben im String. So kann man ihn bspw. unabhängig von Groß-/Kleinschreibung machen.

Video

Quellcode

#include <iostream>
#include <string>
#include <cctype>

struct CaseInsensitiveCharTraits : public std::char_traits<char>
{
    static bool eq(char c, char d)
    {
        return std::tolower(c) == std::tolower(d);
    }
    
    static bool lt(char c, char d)
    {
        return std::tolower(c) < std::tolower(d);
    }
    
    static int compare(const char *p, const char *q, std::size_t n)
    {
        for (std::size_t c = 0; c < n; ++c) {
            if (lt(p[c], q[c])) {
                return -1;
            }
            else if (lt(q[c], p[c])) {
                return 1;
            }
        }
        return 0;
    }
};

int main()
{
    std::basic_string<char, CaseInsensitiveCharTraits> s1 { "Ein kleiner Test" };
    std::basic_string<char, CaseInsensitiveCharTraits> s2 { "Ein KLEINER Test" };
    std::basic_string<char, CaseInsensitiveCharTraits> s3 { "Ein großer Test" };
    
    std::cout << "s1 == s2: " << (s1 == s2) << std::endl;
    std::cout << "s1 == s3: " << (s1 == s3) << std::endl;
    
    auto position = s2.find("kleiner");
    std::cout << "s2.find: " << position << std::endl;
}

Erklärung

Um das Template std::basic_string so unabhängig, wie möglich von der spezifischen Art des Charactertyps zu machen, lagert man alle Operationen, die direkt den Character betreffen in einen eigenen Typ aus und übergibt diesen als Templateparameter. Auf diese Art kann man beispielsweise den Vergleich auf Gleichheit zweier Character delegieren und damit unabhängiger gestalten. Statt c == d in basic_string zu implementieren, ruft diese nur TraitsType::eq(c, d) auf und interpretiert das Ergebnis (TraitsType ist dabei der übergebene Character-Traits-Typ). Wie genau dieser Typ intern den Vergleich anstellt, ist basic_string egal. Hauptsache, er macht es.

Das kann man ausnutzen, um beispielsweise Strings zu implementieren, die sich unabhängig von Groß-/Kleinschreibung vergleichen lassen. Das Beispiel oben demonstriert das. Um die Implementierung von CaseInsensitiveCharTraits kurz zu halten, übernehmen wir mittels Vererbung alles von std::char_traits<char>. Diese Spezialisierung des Standard-Character-Traits bietet eigentlich schon alles für den normalen 8-Bit-Character. Das einzige, was wir anpassen wollen, sind die verschiedenen Vergleichsoperationen, da diese ja nun ohne Beachtung von Groß-/Kleinschreibung arbeiten sollen. Drei Funktionen sind dafür wichtig: eq, welche zwei Characters auf Gleichheit vergleicht, lt, welche vergleicht, ob ein Character kleiner ist, als der andere (bspw. wäre lt('a', 'b') typischerweise true) und compare, welche den Vergleich für ganze Folgen von Characters anstellt. Um uns unabhängig von Groß-/Kleinschreibung zu machen, wandeln wir in diesen Funktionen alles in Kleinbuchstaben um. Dafür benutzen wir die Funktion std::tolower(), welche entweder den Kleinbuchstaben des übergebenen Zeichens zurückliefert oder das Zeichen selbst, wenn es keinen Kleinbuchstaben dazu gibt.

Übergibt man diese Trait-Klasse als entsprechenden Template-Parameter an std::basic_string, dann spielt die Groß-/Kleinschreibung für die Gleichheit keine Rolle mehr. Schön zu sehen ist das am Vergleich von s1 und s2, welche gleich sind, obwohl sie sich im Wort "kleiner" unterscheiden. Genauso kann in dem betreffenden String-Objekt ohne Beachtung von Groß-/Kleinschreibung gesucht werden, wie Zeile 40 zeigt. Das Wort "kleiner" wird gefunden, obwohl es als "KLEINER" im String vorkommt.