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

Konwersja liczb na tekst za pomocą std::to_chars z C++17


2020-05-06, 00:00

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 oraz from_charsaby 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 jak printf/sscanf/itoa, czy inne.

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 oraz to_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:

Materiały o C++17:

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



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


Podobne wpisy


Pssst! Używamy Cookies. Poprzez używanie naszego serwisu zgadzasz się na odczytywanie i zapisywanie Cookies w swojej przeglądarce.
Polityka Prywatności