Skip to content
C++
Library
since C++17
Basic

std::from_chars / std::to_chars

C++17 charconv — locale-independent, non-allocating, non-throwing conversion between numbers and character buffers.

std::from_chars / std::to_charssince C++17

Low-level, locale-independent functions in <charconv> that convert between numbers and character buffers without heap allocation, exceptions, or locale state.

Overview

<charconv> fills a gap that existed since C++98: number-to-string conversion that is both correct and fast. The older alternatives all carry costs that matter in tight loops:

  • std::stoi / std::stod — allocate, throw on error, are locale-sensitive
  • sprintf / sscanf — locale-sensitive, format string parsing overhead
  • std::to_string — allocates, locale-sensitive
  • std::format (C++20) — ergonomic but allocates

to_chars and from_chars avoid all of these. Both are noexcept, operate in the C locale unconditionally, make no heap allocations, and return structured error codes. For floating-point, to_chars without a precision argument produces the shortest decimal representation that round-trips through from_chars exactly — typically via Ryu or Grisu3 internally.

Compiler support caveat. Integer overloads arrived in GCC 8, Clang 7, and MSVC 2017 15.8. Floating-point support came significantly later: GCC 11, Clang 12, and MSVC 2019 16.4. If you target older toolchains, guard float usage with the feature-test macro __cpp_lib_to_chars >= 201611L and verify the compiler version — some implementations define the macro but provide only partial support.

Syntax

cpp
#include <charconv>

// Result types (C++17)
struct to_chars_result {
    char*     ptr;  // one past last written char; equals last on error
    std::errc ec;   // errc{} on success, errc::value_too_large on overflow
};

struct from_chars_result {
    const char* ptr;  // one past last consumed char
    std::errc   ec;   // errc{} on success
};

// Integer overloads (C++17)
to_chars_result   to_chars  (char* first, char* last, /* IntegerT */ value, int base = 10);
from_chars_result from_chars(const char* first, const char* last,
                             /* IntegerT */& value, int base = 10);

// Floating-point overloads (C++17)
to_chars_result   to_chars  (char* first, char* last, /* FloatT */ value);
to_chars_result   to_chars  (char* first, char* last, /* FloatT */ value,
                             std::chars_format fmt);
to_chars_result   to_chars  (char* first, char* last, /* FloatT */ value,
                             std::chars_format fmt, int precision);
from_chars_result from_chars(const char* first, const char* last, /* FloatT */& value,
                             std::chars_format fmt = std::chars_format::general);

// chars_format — bitmask enum (C++17)
enum class chars_format {
    scientific,  // "1.23e+04"
    fixed,       // "1234.5"
    hex,         // hex significand + decimal exponent; NO "0x" prefix
    general,     // shortest of fixed or scientific (default for from_chars)
};

IntegerT is any signed or unsigned integer type including char. FloatT is float, double, or long double. Both functions are noexcept. Neither null-terminates the output buffer. from_chars never skips leading whitespace.

Examples

Integer round-trip

cpp
#include <charconv>
#include <cassert>

// 20 chars covers any int64_t in base 10 (19 digits + sign)
char buf[24];

int64_t val = -9'223'372'036'854'775'807LL;
auto [end, wec] = std::to_chars(buf, buf + sizeof(buf), val);  // C++17
assert(wec == std::errc{});

// Output buffer is NOT null-terminated — use [buf, end) as a view
std::string_view sv{buf, end};  // "-9223372036854775807"

int64_t out;
auto [ptr, rec] = std::from_chars(sv.data(), sv.data() + sv.size(), out);
assert(rec == std::errc{} && out == val);  // exact round-trip guaranteed

Float: shortest vs. explicit precision

cpp
#include <charconv>

char buf[64];
double pi = 3.14159265358979323846;

// No precision arg → shortest decimal that round-trips exactly
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), pi);
// Produces "3.141592653589793" — length is implementation-dependent but always correct

// Fixed decimal places
std::to_chars(buf, buf + sizeof(buf), pi, std::chars_format::fixed, 6);
// "3.141593"

// Scientific
std::to_chars(buf, buf + sizeof(buf), pi, std::chars_format::scientific, 4);
// "3.1416e+00"

// Hex float — note: no 0x prefix, decimal exponent after 'p'
std::to_chars(buf, buf + sizeof(buf), pi, std::chars_format::hex);
// "1.921fb54442d18p+1"

Parsing a delimited record

A typical use case in data pipelines: parsing fixed-format text records without temporary allocations.

cpp
struct Tick {
    int64_t timestamp_ns;
    double  price;
    int32_t quantity;
};

// Parses "1716300000000000000,18432.75,500"
std::optional<Tick> parse_tick(std::string_view line) {
    Tick t;
    const char* p   = line.data();
    const char* end = p + line.size();

    auto r1 = std::from_chars(p, end, t.timestamp_ns);
    if (r1.ec != std::errc{} || r1.ptr == end || *r1.ptr != ',')
        return std::nullopt;

    auto r2 = std::from_chars(r1.ptr + 1, end, t.price);
    if (r2.ec != std::errc{} || r2.ptr == end || *r2.ptr != ',')
        return std::nullopt;

    auto r3 = std::from_chars(r2.ptr + 1, end, t.quantity);
    if (r3.ec != std::errc{})
        return std::nullopt;

    return t;
}

Writing HTTP headers without allocation

cpp
// Appends "Content-Length: <n>\r\n" into an existing char buffer
void append_content_length(std::vector<char>& out, size_t body_size) {
    constexpr std::string_view prefix = "Content-Length: ";
    out.insert(out.end(), prefix.begin(), prefix.end());

    char tmp[24];
    auto [end, ec] = std::to_chars(tmp, tmp + sizeof(tmp), body_size);
    // ec cannot be value_too_large: uint64_t fits in 20 chars, buf is 24
    out.insert(out.end(), tmp, end);
    out.push_back('\r');
    out.push_back('\n');
}

Best Practices

Size your buffer before calling to_chars. There is no convenience wrapper that allocates for you. Safe upper bounds:

TypeBaseMax chars
int32_t1011 (-2147483648)
uint32_t1010
int64_t1020
uint64_t264
double (shortest)1024
double (fixed, p=17)1026

The formula std::numeric_limits<T>::digits10 + 3 covers sign, edge digits, and rounding for integers. For floats use std::numeric_limits<T>::max_digits10 + 8.

Check ec on every call. When to_chars returns errc::value_too_large, ptr == last and the buffer contents are unspecified. When from_chars returns an error, the target variable is left unmodified — safe to test against a default-initialized sentinel.

Chain from_chars calls through ptr. The returned ptr always points to the first unconsumed character, making delimiter-separated parsing a natural fit: advance past the delimiter and feed ptr + 1 into the next call.

Prefer from_chars over stoi/stod in any loop. The performance gap is typically 3–5× for integers and 5–10× for floats, because stoi/stod must acquire the global locale on every call.

Common Pitfalls

to_chars does not null-terminate. The buffer byte at ptr is unmodified. Always construct std::string_view{buf, ptr} or manually write *ptr = '\0' if you need a C-string.

from_chars does not skip leading whitespace. Passing " 42" yields errc::invalid_argument immediately. Advance the pointer past whitespace before calling — the standard provides no built-in facility for this.

from_chars does not accept a leading + sign. "+42" fails; only - is accepted as a sign character for signed types.

Hex integers: no 0x prefix, ever. from_chars(p, end, v, 16) applied to "0xff" parses 0 and stops at x. Strip the prefix before calling.

Hex floats follow the same rule. chars_format::hex reads and writes the mantissa in hexadecimal with a decimal exponent after p. The 0x prefix is not part of the format in either direction.

Float support on older toolchains is silently absent. Some compilers defined __cpp_lib_to_chars before shipping float support. Always test the actual compiler version and run a quick round-trip unit test in CI if you rely on float serialization.

long double quality is implementation-defined. MSVC maps long double to double (80-bit extended precision is not available), and other implementations vary in correctness near edge cases. For portable serialization, double is the safer choice.

See Also