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.
Round-Trip Guarantee and the Shortest-Decimal Invariant
When you call to_chars on a floating-point value without specifying a precision, the standard mandates that it produce the shortest decimal representation that from_chars can parse back to the same bit pattern. In other words:
double original = 3.14159265358979323846;
char buf[32];
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), original);
double recovered;
std::from_chars(buf, end, recovered);
assert(original == recovered); // guaranteed: same bit patternThis guarantee is only valid when both functions come from the same standard library implementation. Mixing, for example, MSVC's to_chars output with GCC's from_chars parser is not covered by the standard (though in practice IEEE 754 decimal is interoperable).
Why "shortest"?
A double has 15–17 significant decimal digits, but the shortest faithful representation is typically 15–16 digits. Outputting all 17 digits wastes space and introduces noise in human-readable output. The standard mandates shortest-faithful since C++17; the algorithms that achieve this (Ryu, Grisu3, Schubfach/Dragonbox) were developed roughly in tandem with the standard's finalization.
The practical algorithm used internally varies by toolchain:
- libstdc++ (GCC 11+): adopted the
fast_floatlibrary forfrom_charsparsing; uses Ryu forto_charsfloat output - libc++ (Clang): uses Ryu for
to_charsfloat output - MSVC: uses its own Ryu-based implementation (earliest to ship complete float support, VS 2019 16.4)
Hex format for exact binary round-trip
chars_format::hex gives a different kind of exactness: the output is a hexadecimal mantissa with a decimal exponent, which is a lossless representation of the IEEE 754 bit pattern with no rounding at all. It is the fastest format to produce and parse, and is ideal for binary serialization formats that still need text portability:
double val = 1.0 / 3.0;
char buf[32];
// Hex: exact binary representation, no rounding
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), val,
std::chars_format::hex);
// Produces something like "1.5555555555555p-2"
// Note: NO "0x" prefix — from_chars::hex also does NOT accept "0x"
double back;
std::from_chars(buf, end, back, std::chars_format::hex);
assert(val == back); // bit-exact, not just decimal-faithfulError Handling In Depth
Both result structs carry two fields — ptr and ec — that together encode three distinct outcomes:
| Outcome | ec | ptr | value |
|---|---|---|---|
| Success | errc{} | first char after the parsed number | updated |
| No match at all | errc::invalid_argument | equals first (nothing consumed) | unchanged |
| Digits parsed but value overflows the target type | errc::result_out_of_range | first char after the digit run | unchanged |
The distinction between the two error codes matters in practice:
invalid_argument: the very first character was not part of a valid number in the requested format.ptr == first. No digits were consumed. Common triggers: leading space, leading+,"0x"prefix on hex integers, or"20.9"whenchars_format::scientificis set (the exponent is required in scientific mode).result_out_of_range: the digits were a valid number syntactically, but the value is outside the range of the target type (e.g."5000000000"intoint).ptradvances past the digits, so you can still detect how many characters were consumed.
#include <charconv>
#include <cassert>
#include <string_view>
// Demonstrates all three from_chars outcomes
void classify(std::string_view s) {
int value = -1; // sentinel: stays -1 on any error
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);
if (ec == std::errc{}) {
// success: value updated, ptr points past last digit
// ptr may be less than s.end() if input had a trailing non-digit
} else if (ec == std::errc::invalid_argument) {
assert(ptr == s.data()); // nothing was consumed
assert(value == -1); // unchanged
} else if (ec == std::errc::result_out_of_range) {
assert(ptr > s.data()); // digits were consumed
assert(value == -1); // still unchanged — do NOT use it
}
}
// classify("42") -> success, value=42
// classify(" 42") -> invalid_argument (leading space)
// classify("+42") -> invalid_argument (leading plus)
// classify("5000000000") -> result_out_of_range (> INT_MAX)
// classify("12abc") -> success, value=12, ptr->"abc" (partial parse)The "partial parse" case
A subtle but important behavior: from_chars stops at the first character that does not fit the pattern and returns success (errc{}). In the example above, "12abc" returns ec == errc{} with ptr pointing at 'a'. If you require the entire input to be consumed, check ptr == last after a successful parse:
std::string_view s = "12abc";
int v;
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), v);
bool fully_consumed = (ec == std::errc{} && ptr == s.data() + s.size());
// fully_consumed == false — trailing "abc" was not parsedC++26: operator bool() on result types (P2497)
C++26 adds a constexpr explicit operator bool() to both from_chars_result and to_chars_result, returning ec == std::errc{}. This allows simpler success checks. Feature-test macro: __cpp_lib_to_chars >= 202306L. Implemented in GCC 14 and Clang 18.
// C++26 style
if (auto res = std::from_chars(p, end, value); res) {
// success
}
// Equivalent C++17 style
if (auto res = std::from_chars(p, end, value); res.ec == std::errc{}) {
// success
}chars_format Deep Dive
chars_format is a bitmask enum; general is defined as fixed | scientific. This has behavioral consequences for both to_chars and from_chars:
Effect on to_chars
| Format | What it produces | Exponent required in output? |
|---|---|---|
general (default) | shorter of fixed or scientific | only if it produces a shorter result |
fixed | "1234.5" | never |
scientific | "1.2345e+03" | always |
hex | "1.3ap+2" (hex mantissa, decimal exponent after p) | always |
When to_chars is called without a fmt argument (shortest-decimal mode), neither general nor any explicit format applies — the implementation picks whatever decimal form is shortest. Passing chars_format::general with a precision is the way to get printf-style %g behavior.
Effect on from_chars
| Format | What it accepts | Rejects |
|---|---|---|
general | fixed or scientific | — |
fixed | "1.5", "-2.0" | any e/E exponent |
scientific | "1.5e+0" (exponent mandatory) | "1.5" without exponent |
hex | "1.8p+1" (hex mantissa, decimal exponent after p) | "0x1.8p+1" (0x prefix rejected) |
The scientific format requiring the exponent is a common surprise: from_chars("20.9", ..., chars_format::scientific) returns errc::invalid_argument because "20.9" has no exponent part.
double v;
// scientific requires exponent — "20.9" has none
auto r1 = std::from_chars("20.9", "20.9" + 4, v, std::chars_format::scientific);
// r1.ec == errc::invalid_argument, r1.ptr == "20.9"
// fixed forbids exponent — "20.9e+0" is partially parsed as 20.9
auto r2 = std::from_chars("20.9e+0", "20.9e+0" + 7, v, std::chars_format::fixed);
// r2.ec == errc{}, v == 20.9, r2.ptr points at "e+0"Performance
<charconv> was designed explicitly for high-throughput scenarios such as JSON parsing and binary protocol interchange. Benchmark data from multiple independent sources:
Integer conversion (from_chars vs alternatives)
From Bartlomiej Filipek's benchmarks in C++17 In Detail (testing from_chars for integer parsing):
- GCC: ~4.5x faster than
stoi, ~2.2x faster thanatoi, ~50x faster thanistringstream - Clang: ~3.5x faster than
stoi, ~2.7x faster thanatoi, ~60x faster thanistringstream - MSVC: ~3x faster than
stoi, ~2x faster thanatoi, ~50x faster thanistringstream
The gap versus stoi comes primarily from locale acquisition: stoi calls strtol which may take a global lock on the locale object on every call. from_chars bypasses this entirely.
Float parsing (from_chars vs strtod)
From Daniel Lemire's benchmarks (parsing random doubles in [0,1]):
| Method | Throughput |
|---|---|
strtod (GCC 12) | 270 MB/s |
from_chars (GCC 12, using fast_float internally) | 1,000 MB/s |
GCC 12's from_chars achieves roughly 4x the throughput of strtod by using the fast_float algorithm (which was also published as a peer-reviewed paper: Number Parsing at a Gigabyte per Second, Lemire, 2021).
Float serialization (to_chars vs sprintf)
From Victor Zverovich's dtoa-benchmark additions (December 2025), testing libc++'s to_chars for double:
to_chars(libc++, Ryu internally): 17x faster thansprintf, and nearly 2x faster than thedouble-conversionlibrary
The gains come from eliminating format-string parsing, locale checks, and the variable-length decimal estimation loops that sprintf uses for floats.
C++23 and C++26 Evolution
C++23: constexpr for integer overloads (P2291)
Integer overloads of both to_chars and from_chars became constexpr in C++23. This enables compile-time number parsing and formatting:
#include <charconv>
#include <optional>
#include <string_view>
constexpr std::optional<int> parse_int(std::string_view sv) {
int value{};
auto [ptr, ec] = std::from_chars(sv.begin(), sv.end(), value);
if (ec == std::errc{})
return value;
return std::nullopt;
}
static_assert(parse_int("42") == 42);
static_assert(parse_int("abc") == std::nullopt);
static_assert(parse_int("99999999999") == std::nullopt); // out of rangeCompiler support: GCC 13, Clang 16, MSVC 19.34 (VS 2022 17.4). Floating-point overloads remain non-constexpr.
C++26: operator bool() on result types (P2497)
Already described in the error-handling section above. Feature-test macro value 202306L for __cpp_lib_to_chars.
C++26 and beyond: wider character type support (P3876, in progress)
WG21 paper P3876R1 (February 2026) proposes extending to_chars and from_chars to support char8_t, char16_t, char32_t, and wchar_t. This matters for applications that use char8_t APIs throughout (e.g. UTF-8 JSON processing) and currently must transcode to char just to call to_chars. The paper was reviewed by SG16 in early 2026 with weak consensus in favor; wording is being finalized against N5032.
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