Konwersja liczb na tekst za pomocą std::to_chars z C++17
W tym artykule przyjrzymy się nowej funkcjonalności do konwersji liczb na tekst z C++17. Za pomocą nowych, niskopoziomowych metod można w łatwy sposób zamienić dowolną liczbę na postać znakową i w dodatku mieć najlepszą z możliwych wydajności!
Wstęp
Przed C++17 mieliśmy takie oto metody na konwersję liczb na tekst:
sprintf
/snprintf
stringstream
to_string
itoa
- inne biblioteki jak boost - lexical cast
Wraz z C++17 mamy kolejną opcję: std::to_chars()
(a także jej “odwortność” std::from_chars()
)! Obydwie funkcje dostępne są w nagłówku <charconv>
.
Dlaczego potrzebujemy nowych metod konwersji? Czy te stare sposoby, dostępne od wielu wielu lat, były niewystarczające?
W skrócie: tak, były niewystarczające, bo to_chars
/from_chars
są bardzo niskopoziomowe i oferują najlepszą z możliwych wydajności.
Nowe cechy:
- nie rzucają wyjątków
- nie allokują nowej pamięci, używają tylko buforów podanych na wejściu
- brak wsparcia dla “locale”, pracują tak jakby tekst był w uniwersalnym “locale”
- są bezpieczne pod kątem dostępu do pamięci, mają sprawdzanie zakresów
- na wyjściu dostajemy wiele informacji o samym procesie konwersji
- gwarancja “round-trip”: można używać
to_chars
orazfrom_chars
aby skonwertować liczbę tam i z powrotem i mamy gwarancję, że dostaniemy za każdym razem tą samą binarną reprezentację. Nie mamy takich zabezpieczeń przy funkcjach jakprintf
/sscanf
/itoa
, czy inne.
- gwarancja “round-trip”: można używać
Prosty przykład:
std::string str { "xxxxxxxx" };
const int value = 1986;
std::to_chars(str.data(), str.data() + str.size(), value);
// str is "1986xxxx"
Nowe metody są dostępne w następujących kompilatorach/bibliotekach:
- Visual Studio 2017 15.9 - pełne wsparcie (
from_chars
orazto_chars
) (notatki do wersji in 15.8 oraz in 15.9)- finalne wsparcie od wersji 16.2
- GCC - 8.0 - w trakcie, tylko wsparcie dla liczb całkowitych
- Clang 7.0 - w trakcie, tylko wsparcie dla liczb całkowitych
Seria o C++17 (po angielsku)
Na moim angielskim blogu ten wpis jest częścią serii o nowościach w Bibliotece Standardowej C++17. Poniżej parę linków:
- Refactoring with
std::optional
- Using
std::optional
- Error handling and
std::optional
- Everything You Need to Know About
std::variant
from C++17 - Everything You Need to Know About
std::any
from C++17 std::string_view
Performance and followup- C++17 string searchers and followup
- Conversion utilities - about from_chars.
- How to get File Size in C++? and std:filesystem::file_size Advantages and Differences
- How To Iterate Through Directories
Materiały o C++17:
- C++17 In Detail - Bartłomiej Filipek
- C++17 - The Complete Guide - Nicolai Josuttis
- C++ Fundamentals Including C++ 17 - Kate Gregory
- Practical C++14 and C++17 Features - Giovanni Dicanio
- C++17 STL Cookbook - Jacek Galowicz
Podstawy to_chars
to_chars()
to zbiór funkcji dla typów floating point oraz całkowitych.
Dla typów całkowitych mamy jedną deklarację:
std::to_chars_result to_chars(char* first, char* last,
TYPE value, int base = 10);
Gdzie TYPE
rozwija się na wszystkie typy całkowito liczbowe ze znakiem i bez oraz na char
.
base
jest z przedziału 2 do 36, cyfry które są większe niż 9 są reprezentowane przez małe literki od a
do z
.
Dla liczb zmiennoprzecinkowych mamy więcej opcji:
Po pierwsze jest podstawowa funkcja:
std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value);
FLOAT_TYPE
rozwija się na float
, double
lub long double
.
Proces konwersji działa tak samo jak printf
ze standardowym “(C) locale”. Finalnie będzie użyty tryb %f
lub %e
w zależności od długość ciągu (krótsza reprezentacja ma pierwszeństwo).
Kolejna funkcja posiada parametr std::chars_format fmt
który pozwala na wybór finalnego formatu tekstu:
std::to_chars_result to_chars(char* first, char* last,
FLOAT_TYPE value,
std::chars_format fmt);
chars_format
to enum o wartościach: scientific
, fixed
, hex
oraz general
(które jest kompozycją fixed
i scientific
).
Na koniec jest jeszcze wersja “pełna” która pozwala na wybranie prezycji - precision
:
std::to_chars_result to_chars(char* first, char* last,
FLOAT_TYPE value,
std::chars_format fmt,
int precision);
Wynik funkcji
Kiedy konwersja jest skuteczna, podany zakres [first, last)
będzie wypełniony skonwertowanym cięgiem znaków.
Zwracana wartość z funkcji (zarówno dla liczb całkowitych jak i zmiennoprzecinkowych) to to_chars_result
, które jest zdefiniowany w następujący sposób:
struct to_chars_result {
char* ptr;
std::errc ec;
};
Typ przechowuje informacja o przeprowadzonym procesie:
Status procesu | Stan w from_chars_result |
---|---|
Sukces | ec jest “value initialised” jako std::errc , oraz ptr jest przesunięty tuż za zapisanym zakresem. Ważna uwaga, że ciąg znaków nie jest zakończony NULL’em!. |
Poza zakresem | ec ma wartość std::errc::value_too_large , zakres [first, last) jest w niezdefiniowanym stanie. |
Podsumowując, mamy w naszym przypadku tylko dwie możliwości: sukces albo “poza zakres” - wtedy kiedy Twój bufor nie ma miejsca na przechowanie wyniku.
Przykład: konwersja typów całkowitoliczbowych
Aby podsumować, poniżej znajdziesz małe demo jak używać to_chars
:
#include <iostream>
#include <charconv> // from_chars, to_chars
#include <string>
int main() {
std::string str { "xxxxxxxx" };
const int value = 1986;
const auto res = std::to_chars(str.data(),
str.data() + str.size(),
value);
if (res.ec == std::errc()) {
std::cout << str << ", filled: "
<< res.ptr - str.data() << " characters\n";
}
else if (res.ec == std::errc::value_too_large) {
std::cout << "value too large!\n";
}
}
Tabelka z przykładowym wejściem i wynikami:
Wartość zmiennej value |
Wynik |
---|---|
1986 |
1986xxxx, filled: 4 characters |
-1986 |
-1986xxx, filled: 5 characters |
19861986 |
19861986, filled: 8 characters |
-19861986 |
value too large! (bufor miał tylko 8 znaków) |
Przykład: liczby zmiennoprzecinkowe
Niestety wsparcie dla liczb zmiennoprzecinkowych jest obecne tylko w kompilatorach Visual Studio - od wersji 15.9, oraz pełne wsparcie od 16.0. GCC oraz Clang obsługują tylko liczby całkowite.
A teraz przykład:
std::string str{ "xxxxxxxxxxxxxxx" }; // 15 chars for float
const auto res = std::to_chars(str.data(),
str.data() + str.size(),
value);
if (res.ec == std::errc()) {
std::cout << str << ", filled: "<< res.ptr - str.data() << " characters\n";
}
else if (res.ec == std::errc::value_too_large) {
std::cout << "value too large!\n";
}
Poniżej tabelka z wejściem oraz wynikami:
Wartość zmiennej value |
Format | Wyjście |
---|---|---|
0.1f |
- | 0.1xxxxxxxxxxxx, filled: 3 characters |
1986.1f |
general | 1986.1xxxxxxxxx, filled: 6 characters |
1986.1f |
scientific | 1.9861e+03xxxxx, filled: 10 characters |
Wydajność i parę liczb
W swojej książce rozwinąłem temat wydajności i przeprowadziłem parę eksperymentów z konwersją licz całkowitych. Nowa funkcjonalność robi wrażenie, bo otrzymałem wzrosty rzędu 10X w porównaniu do to_string
czy sprintf
…lub nawet 23X nad stringstream
!
Jeśli chodzi o wyniki dla liczb zmiennoprzecinkowych to także można zaobserwować około jeden rząd wielkości.
Zachęcam także do obejrzenia prezentacji Stephan T. Lavavej na temat charconv
w MSVC gdzie można dowiedzieć się o jego benchmarkach.
C++20 i nowości w obszarze konwersji liczb!
W C++20 będziemy mieć jeszcze więcej metod do konwersji tekstu i formatowania liczb!
Wszystko dzięki std::format
które jest wzorowane na podstawie popularnej bibliotece do formatowania tekstu {fmt}
.
Tutaj link: https://en.cppreference.com/w/cpp/utility/format
Jak na razie (na początek 2020) żadna biblioteka standardowa nie ma implementacji dla std::format
, ale możemy napisać kod w oparciu o bibliotekę bazową (zgodność jest prawie 100%):
biorąc przykład z https://www.zverovich.net/2019/07/23/std-format-cpp20.html:
std::vector<char> buf;
std::format_to(std::back_inserter(buf), "{}", 42);
// buf contains "42"
Możesz także przeczytać artykuł na temat std::format
/ {fmt}
na moim blogu:
An Extraterrestrial Guide to C++20 Text Formatting
Podsumowanie
Wraz z C++17 dostaliśmy nową funkcjonalność która zapewnia niskopoziomową konwersję tekstu i liczb. Nowe funkcje są bardzo szybkie oraz zwracają wiele informacji o samym procesie konwersji. Dodatkowo te funkcje nie allokują extra pamięci a także nie rzucają wyjątków!
Przeczytaj także o odpowiedniku from_chars
do konwersji z tekstu na liczbę.
A tutaj link do prezentacji Stephan’a o trudnościach w implementacji tej funkcjonalności, oraz dlaczego tak długo zajmuje dołożenie wsparcia dla licz zmiennoprzecinkowych:
Floating-Point <charconv>
: Making Your Code 10x Faster With C++17’s Final Boss by Stephan T. Lavavej