Ranges und String Split



  • Hi,

    eigentlich eine typische Aufgabe: einen String an einem bestimmten Zeichen splitten.
    Ich habe das mal genutzt um ein bisschen mit std::ranges zu spielen und kann mir das Verhalten nicht erklären.

    Ich splitte ein String an dem Zeichen "/".
    split3 - einfach durchiterieren und ausgeben funktioniert wie man das erwarten würde.
    split1 - dreht die Reihenfolge, kann mir jemand erklären warum? sry, war dumm - Iterator wird inkrementiert und so
    split2 - findet den Trenner nicht und gibt daher als erste Position den ganzen String aus. Ich verstehe, dass ein String eine Range auf char ist und man daher einen char braucht, hätte aber erwartet, dass das dann ein Fehler gibt, da die Typen nicht zusammen passen.

    #include <ranges>
    #include <iostream>
    
    constexpr void split1(const std::string& key)
    {
     auto parts = std::views::split(key,'/');
     if (parts.empty())
       std::cout << "empty";
    
     auto first = parts.begin();
     const auto second = first++;
    
     if (second == std::end(parts))
       std::cout << std::ranges::to<std::string>(*first) << " \n";
    
      std::cout << std::ranges::to<std::string>(*first) << " " <<std::ranges::to<std::string>(*second) << " \n";
    }
    
    constexpr void split2(const std::string& key)
    {
     auto parts = std::views::split(key,"/");
     if (parts.empty())
       std::cout << "empty";
    
     auto first = parts.begin();
     const auto second = first++;
    
     if (second == std::end(parts))
       std::cout << std::ranges::to<std::string>(*first) << " \n";
    
      std::cout << std::ranges::to<std::string>(*first) << " " <<std::ranges::to<std::string>(*second) << " \n";
    }
    
    constexpr void split3(const std::string& key)
    {
     for(auto part : std::views::split(key, '/'))
       std::cout << std::string_view(part) << " ";
     
    }
    
    int main()
    {
     std::string key("test/bla");
     split1(key);
     split2(key);
     split3(key);
    }
    

    Link zum direkt rumspielen: https://godbolt.org/z/sqW58czas



  • Benutze in split2

    using std::operator""sv;
    auto parts = std::views::split(key,"/"sv);
    

    wie auch im Beispiel zu std::ranges::views::split verwendet wird.

    Die Erklärung steht unter "Notes":

    The delimiter pattern generally should not be an ordinary string literal, as it will consider the null terminator to be necessary part of the delimiter; therefore, it is advisable to use a std::string_view literal instead.



  • @Th69 Ok, verstehe ich.

    Aber das ist doch Kontraintuitiv. Man versucht mit allen Mitteln C++ einfacher und weniger Fehleranfällig zu machen und dann macht man sowas.



  • @Schlangenmensch sagte in Ranges und String Split:

    @Th69 Ok, verstehe ich.

    Aber das ist doch Kontraintuitiv. Man versucht mit allen Mitteln C++ einfacher und weniger Fehleranfällig zu machen und dann macht man sowas.

    Vielleicht wollte man erlauben, an '\0' zu splitten, was ja sonst nicht möglich wäre.


  • Mod

    @wob sagte in Ranges und String Split:

    Vielleicht wollte man erlauben, an '\0' zu splitten, was ja sonst nicht möglich wäre.

    Wieso nicht? Gerade mit string_view-Literalen wäre es ein leichtes, '\0' im Delimiter zu haben - und sogar an beliebiger Stelle.

    Ich vermute eher, dass man konsistent bleiben möchte zu all den modernen Sprachmitteln, und kein Sondersüppchen für C-Stringliterale wollte, dass bei denen und nur bei denen das letzte Zeichen nicht gilt. Das ist ziemlich unschön, aber zumindest kommt dann von der modernen C++-Seite her nur eine Regel, die konsistent angewendet wird. Die Sonderungen der C-Stringliterale kommen halt aus der Geschichte. Leider ist damit die einfache und intuitive Art ein Stringliteral zu schreiben mit historischen Sonderregeln verseucht, aber die Syntax von C-Stringliteralen und Stringviewliteralen zu vertauschen hat man sich dann (zurecht!) doch nicht getraut.



  • @SeppJ sagte in Ranges und String Split:

    Wieso nicht? Gerade mit string_view-Literalen wäre es ein leichtes, '\0' im Delimiter zu haben - und sogar an beliebiger Stelle.

    Jetzt verwirrst du mich. Ist das nicht genau das, was ich geschrieben habe?

    Das wieso nicht: weil man in C-Strings kein \0 als Zeichen haben kann. Wie soll ich mit einem C-String sagen, dass ich den String { 'h', 'a', 'l', 'l', 'o', 'x', '0x00', 'x', 'S', 'e', 'p', 'p', 'J' } and "x\0x" splitten will - geht halt nicht, weil das dasselbe wie "x" wäre (also nach C-String-Regeln, jetzt nicht pointer vs array)


  • Mod

    @wob sagte in Ranges und String Split:

    @SeppJ sagte in Ranges und String Split:

    Wieso nicht? Gerade mit string_view-Literalen wäre es ein leichtes, '\0' im Delimiter zu haben - und sogar an beliebiger Stelle.

    Jetzt verwirrst du mich. Ist das nicht genau das, was ich geschrieben habe?

    Ach, da habe ich dich falsch verstanden! Ich dachte, du wolltest damit erklären, wieso klassische C-Stringliterale sich so verhalten, wie sie es tun



  • @wob sagte in Ranges und String Split:

    Das wieso nicht: weil man in C-Strings kein \0 als Zeichen haben kann. Wie soll ich mit einem C-String sagen, dass ich den String { 'h', 'a', 'l', 'l', 'o', 'x', '0x00', 'x', 'S', 'e', 'p', 'p', 'J' } and "x\0x" splitten will - geht halt nicht, weil das dasselbe wie "x" wäre (also nach C-String-Regeln, jetzt nicht pointer vs array)

    Das habe ich tatsächlich vor einiger Zeit benötigt: Für mein DOS-Hobbyprojekt liegen z.B. die Umgebungsvariablen in einem Speichersegment mit KEY=VALUE Paaren, die jeweils durch einen 0-char getrennt sind. Ich kann da einen string_view über den gesamten Speicherbereich erstellen und gentenv mithilfe von find_if und split/transform views elegant als einzelnes return-Statement implementieren. Modernes C++ macht richtig Spass, und Ranges sind ein wichtiges Element davon 😉

    Und wo wir gerade bei Ranges sind: Ich weiß nicht ob das bereits ein gängiges "Pattern" ist, aber ich bin gerade dazu übergegangen Ranges dazu zu nutzen, meine Interfaces sauberer zu definieren: Wenn ich z.B. eine Kollektion von Objekten zurückgebe, dann verpacke ich diese in eine generische Range anstatt z.B. eine Referenz oder Kopie auf den internen std::vector zurückzugeben. Der Grund dafür ist, dass ich finde, dass ein Interface keine internen Implementierungs-Details nach außen "lecken" sollte wie z.B. ein std::vector-Objekt mit all seinen Membern. Das hat auch den Vorteil, dass das Interface stabil bleibt, wenn ich die interne Repräsentation wechsle (z.B. von std::vector<T> auf std::vector<std::unique_ptr<T>>, std::list<T>, oder was auch immer). Die Funktion gibt immer ein Objekt zurück, welches das std::ranges::range<T>-Konzept erfüllt und auch nur die minimale Funktionalität dieses Konzepts unterstützt.



  • Was mir im Zuge dessen wieder aufgefallen ist, ist das Paketalter bei Ubuntu.
    Ich arbeite unter Windows mit dem mscv, der ist, was neue Features betrifft sehr gut. Aber, zumindest einen Teil, muss ich auch unter Linux kompilieren, was ich aktuell unter Ubuntu mache.
    In den mitgelieferten Paketen kann libstdc++ kein std::ranges::to und libc++ kein std::ranges::views::enumerate. Tatsächlich kann libc++ das wohl auch in der aktuellen Version nicht.



  • @Schlangenmensch Bei den Standard-"Systemcompilern" sind die Distributionen auch eher konservativ (=älter und stabiler). Schau mal im Paketmanager, ob du vielleicht eine aktuellere Version zusätzlich installieren kannst. Eventuell aus einem speziellen Repository.

    Laut Support-Matrix wird views::enumerate tatsächlich noch nicht von libc++ unterstützt und und std::ranges::to von libstc++ erst ab GCC 15 vollständig. Aktuell ist derzeit GCC 15.2. Das arch-basierte CachyOS das ist derzeit nutze hat 15.2.1 installiert. Diese Distribution legt aber aber besonderen Fokus auf aktuelle Versionen.

    Aber je nachdem mit was man experimentieren will, ist selbst der aktuelle Release "zu alt": Ich spiele gerade etwas mit C++26 Reflection und Annotations herum. Dafür musste ich mir nen GCC aus nem privaten GCC-Entwickler-Repository bauen. Das ist schon ziemlich weit fortgeschritten übrigens. Gut möglich, dass das nächste GCC Release Reflection unterstützt (wen's interessiert).

    Wenn ich die Commits richtig gelesen habe ist es gerade gestern "fertig" geworden für die GCC 16 Stage 3, die am Montag beginnt. Das ist Voraussetzung dafür, dass es überhaupt aufgenommen wird, weil in der nächsten Phase nur noch Bugfixes erlaubt sind.


Anmelden zum Antworten