Czy wiesz, że jesteśmy również na Slacku? Dołącz do nas już teraz klikając tutaj!

Jak używać najnowszych konwersji string-ów - std::from_chars


2019-01-03, 01:43

Wraz ze standardem C++17 przychodzi nowy mechanizm do obsługi konwersji pomiędzy tekstem a liczbami. Dlaczego powinniśmy stosować nowe algorytmy? Czy są lepsze od poprzednich?

Wstęp

Przed wprowadzeniem standardu C++17, język C++ oferował kilka metod konwersji ciągów znakowych:

  • sprintf / snprintf
  • sscanf
  • atol
  • strtol
  • strstream
  • stringstream
  • to_string
  • stoi and similar functions

Wraz z C++17 otrzymujemy nową opcję: std::from_chars! Czy poprzednie rozwiązania nie były wystarczająco dobre? Dlaczego potrzebujemy nowych algorytmów?

W skrócie: ponieważ std::from_chars jest nisko-poziomowy, dzięki czemu oferuje znacznie lepszą wydajność.

Nowe algorytmy konwersji:

  • nie rzucają wyjątków
  • nie alokują dynamicznie pamięci
  • nie obsługują znaków diakrytycznych
  • są bezpieczne pamięciowo
  • generują błędy, które dają nam więcej informacji o wyniku konwersji

Ich API może nie jest w większości tak przyjazne jak poprzednie, ale bardzo łatwo jest je otoczyć fasadą.

Mały przykład:

const std::string str { "12345678901234" };
int value = 0;
std::from_chars(str.data(),str.data() + str.size(), value);
// pomijamy sprawdzanie błędów...

Nowe algorytmy są dostępne w następujących wersjach kompilatorów:

  • Visual Studio 2017 15.9 - pełne wsparcie (zarówno from_chars jak i to_chars) (sprawdź zmiany w wersjach 15.8 oraz 15.9)
  • GCC - 8.0 - prace w trakcie, aktualnie wspierane są jedynie liczby całkowite
  • Clang 7.0 - prace w trakcie, aktualnie wspierane są jedynie liczby całkowite

Jeżeli chcesz przeczytać więcej o istniejących metodach konwersji, nowych metodach oraz zobaczyć kilka benchmarków, możesz przeczytać dwa świetne posty na @fluentcpp:
How to Convert a String to an int in C++ and How to Efficiently Convert a String to an int in C++ autorstwa JFT.

Przyglądnijmy się teraz API nowych algorytmów.

Konwersja znaków na liczby: std::from_chars

Rodzina std::from_chars to zestaw przeładowanych funkcji: dla typów całkowitych oraz zmiennoprzecinkowych.

Dla typów całkowitych mamy funkcje:

std::from_chars_result from_chars(
    const char* first,
    const char* last,
    TYPE &value,
    int base = 10
);

Gdzie TYPE jest rozwijane do wszystkich dostępnych typów całkowitych (ze znakiem i bez znaku) oraz typ char.

Parametr base (podstawa systemu liczbowego) może przyjmować wartości od 2 do 36.

Tutaj mamy wersję dla typów zmiennoprzecinkowych:

std::from_chars_result from_chars(
    const char* first,
    const char* last,
    FLOAT_TYPE& value,
    std::chars_format fmt = std::chars_format::general
);

Typ FLOAT_TYPE rozwijany jest do typów float, double oraz long double.

Parametr chars_format jest wartością enum o wartościach: scientific, fixed, hex oraz general (która jest kompozycją fixed oraz scientific).

Wartość zwracana przez wszystkie te funkcje (dla wszystkich typów całkowitych jak i dla typów zmiennoprzecinkowych) jest typu from_chars_result:

struct from_chars_result {
    const char* ptr;
    std::errc ec;
};

Struktura from_chars_result przechowuje wartościowe informacje o procesie konwersji.

Podsumowując:

Warunek zwrotu Stan wartości typu from_chars_result
Sukces Pole ptr wskazuje na pierwszy znak niedopasowany do wzorca lub posiada wartość last jeżeli wszystkie znaki zostały dopasowane oraz jeśli pole ec jest zainicjalizowane wartością.
Błąd Pole ptr wskazuje na first, a ec ma wartość std::errc::invalid_argument. Pole value zostaje niezmodyfikowane.
Poza zakresem Liczba jest zbyt duża, aby mogła być przechowywana wewnątrz typu value. Pole ec ma wartość std::errc::result_out_of_range, a ptr wskazuje na pierwszy znak niedopasowany do wzorca. Pole value zostaje niezmodyfikowane.

Nowe algorytmy są sprecyzowane na na prawdę niskim poziomie, zatem możesz zapytać dlaczego? Titus Winters dodał świetne podsumowanie w komentarzu¹:

The intent of those APIs is not for people to use them directly, but to build more interesting/useful things on top of them. These are primitives, and we (the committee) believe they have to be in the standard because there isn’t an efficient way to do these operations without calling back out to nul-terminated C routines

Przykłady

Poniżej przedstawiam dwa przykłady na to, jak konwertować stringi na liczby używając from_chars oraz jak zapisywać je w zmiennych typu int oraz float.

Typy całkowite

#include <charconv> // from_char, to_char
#include <string>
#include <iostream>

int main()
{
    const std::string str { "12345678901234" };
    int value = 0;
    const auto res = std::from_chars(
        str.data(), str.data() + str.size(), value
    );

    if (res.ec == std::errc())
    {
        std::cout << "wartość: " << value 
                  << ", dystans: " << res.ptr - str.data() << '\n';
    }
    else if (res.ec == std::errc::invalid_argument)
    {
        std::cout << "niepoprawna wartość!\n";
    }
    else if (res.ec == std::errc::result_out_of_range)
    {
        std::cout << "poza zakresem! dystans res.ptr: " 
                  << res.ptr - str.data() << '\n';
    }
}

Jest to bezpośredni przykład, który przesyła string do from_chars oraz wyświetla wynik wraz z dodatkowymi informacjami, jeśli te są dostępne.

Kod możesz uruchomić tutaj, na Coliru: demo link.

Czy “12345678901234” może zostać skonwertowane do typu całkowitego? Spróbuj sobie skompilować kod i sobaczyć co zwraca.

Typy zmiennoprzecinkowe

Aby przeprowadzić test na liczbach zmiennoprzecinkowych, możemy zmienić kilka pierwszych linijek z poprzedniego przykładu:

// póki co działa jedynie z MSVC (Grudzień 2018)
const std::string str { "16.78" };
double value = 0;
const auto format = std::chars_format::general;
const auto res = std::from_chars(
    str.data(), str.data() + str.size(), value, format
);

Tutaj mamy wyjście, którego możemy się spodziewać:

wartość str wartość formatu wyjście
1.01 fixed wartość: 1.01, dystans 4
-67.90000 fixed wartość: -67.9, dystans: 9
20.9 scientific niepoprawna wartość!, res.ptr dystans: 0
20.9e+0 scientific wartość: 20.9, dystans: 7
-20.9e+1 scientific wartość: -209, dystans: 8
F.F hex wartość: 15.9375, dystans: 3
-10.1 hex wartość: -16.0625, dystans: 5

Format general jest kombinacją dwóch wartości: fixed oraz scientific, zatem obsługuje zarówno regularne wartości zmiennoprzecinkowe, jak i te ze składnią e+num.

Słowo na temat wydajności

Zrobiłem mały benchmark, który pokazał, że nowe algorytmy są błyskawiczne!
Kilka liczb:

  • Na GCC są ~4,5x szybsze niż stoi, 2,2x szybsze niż atoi i prawie 50x szybsze niż istringstream.
  • Na Clang są ~3,5x szybsze niż stoi, 2.7x szybsze niż atoi i 60x szybsze niż istringstream!
  • MSVC przetwarza ~3x szybciej niż stoi, ~2x szybciej niż atoi i prawie 50x szybciej niż istringstream

Powyższy benchmark jest dobrym tematem na osobny wpis, więc póki co nie udostępniam go.

Podsumowanie

Jeżeli chcecie konwertować tekst na liczby oraz nie potrzebujecie dodatkowych rzeczy takich jak wsparcie znaków diakrytycznych, to std::from_chars może okazać się najlepszym wyborem. Oferuje ono sporą wydajność, i co więcej, otrzymamy dużą ilość informacji na temat procesu konwersji (na przykład ile znaków zostało przeskanowanych).

Nowe algorytmy mogą być szczególnie przydatne, kiedy parsujemy pliki typu JSON, pliki z teksturami 3D (np. pliki typu OBJ) itp.

Małe wtrącenie, cały rozdział o konwersjach, benchmarkach, from_chars, to_chars, przykładach… jest dostępny w książce Bartka: C++17 In Detail.

Pytania do Was:

Czy kiedykolwiek bawiliście się nowymi algorytmami konwersji? Kiedy zazwyczaj potrzebowaliście konwertować tekst na liczby?


¹ - Treść zostawiona w formie oryginalnej



Bartłomiej Filipek

Programista i pasjonat C++ z ponad 11-letnim doświadczeniem. Bloguje od wielu lat, głównie o naszym ulubionym języku programowania. Autor ksiązki C++17 In Detail.

Blog Bartka
Profil na LinkedIn
Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.