Jak używać najnowszych konwersji string-ów - std::from_chars
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 ito_chars
) (sprawdź zmiany w wersjach 15.8 oraz 15.9)- Ciągle dochodzą nowe elementy wymagające implementacji dla
to_chars
; przeczytaj ten rozległy komentarz na r/cpp autorstwa Stephan T. Lavavej aby dowiedzieć się więcej.
- Ciągle dochodzą nowe elementy wymagające implementacji dla
- 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