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++17Low-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-sensitivesprintf/sscanf— locale-sensitive, format string parsing overheadstd::to_string— allocates, locale-sensitivestd::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
#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
#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 guaranteedFloat: shortest vs. explicit precision
#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.
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
// 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:
| Type | Base | Max chars |
|---|---|---|
int32_t | 10 | 11 (-2147483648) |
uint32_t | 10 | 10 |
int64_t | 10 | 20 |
uint64_t | 2 | 64 |
double (shortest) | 10 | 24 |
double (fixed, p=17) | 10 | 26 |
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
std::format— C++20, allocates, locale-optional, ergonomic alternativestd::string_view— zero-copy input type forfrom_charscallersstd::errc— the error-code enum returned in both result typesstd::numeric_limits— compile-time constants for buffer sizing