Table of Contents

In this post, I’ll show you how to use the newest, low-level, conversion routines form C++17. With the new functionality, you can quickly transform numbers into text and have super performance compared to previous techniques.

Before C++17  

Until C++17, we had several ways of converting numbers into strings:

  • sprintf / snprintf
  • stringstream
  • to_string
  • itoa
  • and 3rd-party libraries like boost - lexical cast

And with C++17 we get another option: std::to_chars (along with the corresponding method from_chars) ! The functions both reside in the <charconv> header.

Why do we need new methods? Weren’t the old technique good enough?

In short: because to_chars and from_chars are low-level, and offers the best possible performance.

The new conversion routines are:

  • non-throwing
  • non-allocating
  • no locale support
  • memory safety
  • error reporting gives additional information about the conversion outcome
  • bound checked
  • explicit round-trip guarantees - you can use to_chars and from_charsto convert the number back and forth, and it will give you the exact binary representations. This is not guaranteed by other routines like printf/sscanf/itoa, etc.

A simple example:

std::string str { "xxxxxxxx" };
const int value = 1986;
std::to_chars(str.data(), str.data() + str.size(), value);

// str is "1986xxxx"

The new functions are available in the following compilers:

  • Visual Studio 2019 16.4 - full support, and early floating-point support from VS 2017 15.7
  • GCC - 11.0 - full support, and since GCC 8.0 - integer support only
  • Clang 7.0 - still in progress, only integer support

The Series  

This article is part of my series about C++17 Library Utilities. Here’s the list of the articles:

Resources about C++17 STL:

Using to_chars  

to_chars is a set of overloaded functions for integral and floating-point types.

For integral types there’s one declaration:

std::to_chars_result to_chars(char* first, char* last,
                              TYPE value, int base = 10);

Where TYPE expands to all available signed and unsigned integer types and char.

Since base might range from 2 to 36, the output digits that are greater than 9 are represented as lowercase letters: a...z.

For floating-point numbers, there are more options.

Firstly there’s a basic function:

std::to_chars_result to_chars(char* first, char* last, FLOAT_TYPE value);

FLOAT_TYPE expands to float, double or long double.

The conversion works the same as with printf and in default (“C”) locale. It uses %f or %e format specifier favouring the representation that is the shortest.

The next function overload adds std::chars_format fmt that let’s you specify the output format:

std::to_chars_result to_chars(char* first, char* last, 
                              FLOAT_TYPE value,
                              std::chars_format fmt);

chars_format is an enum with the following values: scientific, fixed, hex and general (which is a composition of fixed and scientific).

Then there’s the “full” version that allows also to specify precision:

std::to_chars_result to_chars(char* first, char* last, 
                              FLOAT_TYPE value,
                              std::chars_format fmt, 
                              int precision);

The Output  

When the conversion is successful, the range [first, last) is filled with the converted string.

The returned value for all functions (for integer and floating-point support) is to_chars_result, it’s defined as follows:

struct to_chars_result {
    char* ptr;
    std::errc ec;
};

The type holds information about the conversion process:

Return Condition State of from_chars_result
Success ec equals value-initialized std::errc and ptr is the one-past-the-end pointer of the characters written. Note that the string is not NULL-terminated.
Out of range ec equals std::errc::value_too_large the range [first, last) in unspecified state.

As you can see, we have only two options: success or out of range - as there’s a chance your buffer doesn’t have enough size to hold the result.

An Example - Integer types  

To sum up, here’s a basic demo of 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";
    }
}

Below you can find a sample output for a set of numbers:

value value output
1986 1986xxxx, filled: 4 characters
-1986 -1986xxx, filled: 5 characters
19861986 19861986, filled: 8 characters
-19861986 value too large! (the buffer is only 8 characters)

An Example - Floating Point  

On MSVC (starting from 15.9, full support in 16.0 + improvements later) and GCC 11.0 we can also try the floating-point support:

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";
}

And here’s a working demo under GCC 11.0:

#include <iostream>
#include <charconv> // from_chars, to_chars
#include <string>

int main() {
    std::string str { "xxxxxxxx" };
    const double value = 1986.10;
 
    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 {
        std::cout << "value too large!\n";
    }
}

Play with code @Compiler Explorer

Below you can find a sample output for a set of numbers:

value value format output
0.1f - 0.1xxxxxxxxxxxx, filled: 3 characters
1986.1f general 1986.1xxxxxxxxx, filled: 6 characters
1986.1f scientific 1.9861e+03xxxxx, filled: 10 characters

Benchmark & Some numbers  

In my book - C++17 in Detail - I did some perf experiments for integer conversions, and the new functionality is several times faster than to_string or sprintf and more than 10… or even 23x faster than stringstream versions!

I also have to check the floating-point support, but the results that I see from various places also claim order of magnitude speedup over the older techniques.

See the Stephan T. Lavavej’s talk (in references) about implementing charconv in MSVC where he shared some floating-point benchmark results.

C++20  

In C++20, we have more methods that allow us to convert data into strings and format them.

The library is called std::format and is based on a popular framework {fmt}

Have a look: https://en.cppreference.com/w/cpp/utility/format

As of today (June 2021) you can play with the library under MSVC 16.10 (VS 2019):

std::vector<char> buf;
std::format_to(std::back_inserter(buf), "{}", 42);
 // buf contains "42"

You can also check out this blog post that nicely introduces you to the concepts of std::format:
An Extraterrestrial Guide to C++20 Text Formatting - C++ Stories

As for the benchmarks you can read this one: Converting a hundred million integers to strings per second - it includes comparison to to_chars and shows various results for integer conversions.

Summary  

With C++17, we got new functionality that allows easy and low-level conversions between numbers and text. The new routines are potent and expose all the information you need to create advanced parsers or serialises. They won’t throw, won’t allocate, they bound check, and they offer super performance.

Read here about the corresponding from_chars method

Extra: since CppCon 2019 Microsoft opened their STL implementation, so you can even have a look at the code of charconv!

I also highly suggest watching Stephan’s talk about the progress and the efforts for full charconv support. The feature looked very simple at first sight, but it appeared to be super complicated to support as the C library couldn’t be used, and everything had to be done from scratch.

Floating-Point <charconv>: Making Your Code 10x Faster With C++17’s Final Boss by Stephan T. Lavavej

Your Turn

What do you think about the new conversion routines? Have you tried them?
What other conversion utilities do you use?