String Conversions and std::locale
Fast, locale-independent parsing with from_chars/to_chars (C++17), convenient stoi/stod (C++11), and locale-aware formatting via std::locale and its facets.
String Conversion & std::localesince C++11C++ offers two tiers for numeric/string conversion: the fast, locale-independent <charconv> primitives (from_chars/to_chars, C++17) and the convenient-but-slower <string> family (stoi/stod/to_string, C++11), with std::locale governing all locale-sensitive formatting and classification.
Overview
The standard library provides four distinct conversion layers:
<charconv>primitives (C++17) βfrom_charsandto_chars. No allocation, no locale, no exceptions. The fastest portable option; default choice for performance-sensitive or safety-critical parsing.<string>family (C++11) βstoi,stol,stod,to_string. Convenient, throw on error, locale-sensitive for floating-point decimal separators.<cstdio>functions βsscanf,sprintf,atoi. Legacy C API with poor error reporting. Avoid in new code.std::localeand facets β Encapsulates formatting rules for numbers, time, money, collation, and character classification. Used directly for locale-aware I/O or string operations.
The critical distinction: from_chars/to_chars always use "C" locale semantics (dot as decimal separator, no thousands grouping). The stod/stof functions delegate to strtod, which uses the current C locale, making them locale-sensitive for the decimal separator. This subtle difference has caused bugs in codebases that mix locale settings with float parsing.
Syntax
// <charconv> β C++17
std::from_chars_result from_chars(const char* first, const char* last,
T& value, int base = 10);
std::from_chars_result from_chars(const char* first, const char* last,
FloatT& value,
std::chars_format fmt = std::chars_format::general);
struct from_chars_result { const char* ptr; std::errc ec; };
std::to_chars_result to_chars(char* first, char* last, T value, int base = 10);
std::to_chars_result to_chars(char* first, char* last, FloatT value,
std::chars_format fmt);
std::to_chars_result to_chars(char* first, char* last, FloatT value,
std::chars_format fmt, int precision);
struct to_chars_result { char* ptr; std::errc ec; };
// <string> β C++11
int stoi (const std::string& str, std::size_t* pos = nullptr, int base = 10);
long stol (const std::string& str, std::size_t* pos = nullptr, int base = 10);
long long stoll(const std::string& str, std::size_t* pos = nullptr, int base = 10);
float stof (const std::string& str, std::size_t* pos = nullptr);
double stod (const std::string& str, std::size_t* pos = nullptr);
std::string to_string(/* integral or floating-point */ val);Examples
from_chars and to_chars (C++17)
#include <charconv>
#include <string_view>
// Integer parsing β all implementations since C++17
std::string_view sv = "12345";
int n;
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), n);
if (ec == std::errc{}) {
// success; ptr points one past the last consumed character
} else if (ec == std::errc::invalid_argument) {
// no valid digits at start of input
} else if (ec == std::errc::result_out_of_range) {
// value overflows int
}
// Detect partial consumption (useful when full input is required)
if (ptr != sv.data() + sv.size()) { /* trailing garbage */ }
// Alternative bases
int hex_val, bin_val;
std::from_chars("ff", "ff" + 2, hex_val, 16); // 255
std::from_chars("1010", "1010" + 4, bin_val, 2); // 10
// Floating-point β C++17 standard, but implementation lagged:
// MSVC 2019 16.4+, GCC 11+, Clang 14+. Gate with __cpp_lib_to_chars if needed.
double d;
std::from_chars("3.14159", "3.14159" + 7, d);
// Produces the exact double nearest 3.14159 with round-trip guarantee
// Specific formats via std::chars_format
std::from_chars("3.14e2", "3.14e2" + 6, d, std::chars_format::scientific); // 314.0
std::from_chars("1.8p+1", "1.8p+1" + 6, d, std::chars_format::hex); // 3.0
// from_chars does NOT skip leading whitespace β unlike strtol/sscanf
// " 42" β ec == errc::invalid_argument#include <charconv>
#include <string_view>
char buf[32];
// Integer
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), 12345);
std::string_view result{buf, end}; // "12345"
// Hex β lowercase by specification
std::to_chars(buf, buf + sizeof(buf), 255, 16); // "ff"
// Float: shortest decimal that round-trips back to the exact same double
std::to_chars(buf, buf + sizeof(buf), 3.14159265358979);
// Fixed precision
std::to_chars(buf, buf + sizeof(buf), 3.14159, std::chars_format::fixed, 2); // "3.14"
std::to_chars(buf, buf + sizeof(buf), 3.14159, std::chars_format::scientific); // "3.14159e+00"
std::to_chars(buf, buf + sizeof(buf), 3.14159, std::chars_format::hex); // hex float
// Zero-allocation boundary wrapper
std::string num_to_str(double val) {
char buf[32];
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), val);
return {buf, end}; // single allocation at the output boundary only
}stoi / stod Family (C++11)
#include <string>
#include <stdexcept>
// Integer β recognizes only ASCII digits regardless of locale
int n = std::stoi("42");
long l = std::stol("-1234567890");
long long ll = std::stoll("9876543210");
unsigned long ul = std::stoul("4294967295");
// Alternative bases
int hex = std::stoi("ff", nullptr, 16); // 255
int oct = std::stoi("77", nullptr, 8); // 63
int bin = std::stoi("1010", nullptr, 2); // 10
// idx receives the count of characters consumed; use it to detect trailing garbage
std::size_t idx;
int val = std::stoi("42abc", &idx); // val=42, idx=2
if (idx != str.size()) { /* partial parse */ }
// Float β stod/stof call strtod, which IS locale-sensitive for decimal point.
// In a German locale (decimal=','), stod("3.14") may throw or return wrong value.
// Use from_chars when locale-independence is required.
double d = std::stod("3.14159");
float f = std::stof("3.14");
long double ld = std::stold("3.14159265358979323846");
// Both integer and float variants throw on failure
try {
std::stoi("not_a_number"); // throws std::invalid_argument
std::stoi("99999999999999999999"); // throws std::out_of_range
} catch (const std::invalid_argument&) { /* input did not start with a digit */ }
catch (const std::out_of_range&) { /* overflow */ }to_string (C++11)
#include <string>
#include <format> // C++20
std::string si = std::to_string(42); // "42"
std::string sf = std::to_string(3.14); // "3.140000" β always 6 decimal places, like %f
std::string sb = std::to_string(true); // "1", not "true"
std::wstring wsi = std::to_wstring(42); // L"42" β wstring variant, C++11
// For anything beyond default format, prefer std::format (C++20):
std::string fixed = std::format("{:.2f}", 3.14159); // "3.14"
std::string sci = std::format("{:e}", 3.14159); // "3.141590e+00"
std::string boolean = std::format("{}", true); // "true", not "1"
// std::format also produces shortest round-trip float by default: std::format("{}", 3.14)std::locale and Facets
#include <locale>
#include <sstream>
#include <iomanip>
std::locale classic = std::locale::classic(); // "C" locale β always available
std::locale sys = std::locale(""); // system locale from LC_ALL/LANG
// Named locales β platform-dependent availability; throws std::runtime_error if absent
std::locale us("en_US.UTF-8");
std::locale de("de_DE.UTF-8");
// Imbue a stream β affects number, money, and time formatting
std::ostringstream oss;
oss.imbue(us);
oss << std::fixed << std::setprecision(2) << 1234567.89;
// oss.str() == "1,234,567.89"
oss.str("");
oss.imbue(de);
oss << std::fixed << std::setprecision(2) << 1234567.89;
// oss.str() == "1.234.567,89"
// locale::global() sets the global C++ locale and syncs the C locale (setlocale).
// NOT thread-safe if other threads use locale-sensitive operations simultaneously.
std::locale::global(std::locale("en_US.UTF-8"));#include <locale>
#include <string>
// Query formatting rules via facets without stream I/O
const auto& np = std::use_facet<std::numpunct<char>>(std::locale("en_US.UTF-8"));
char tsep = np.thousands_sep(); // ','
char dsep = np.decimal_point(); // '.'
std::string g = np.grouping(); // "\3" β each byte is a group SIZE, not an ASCII digit
// Locale-aware collation β sorts "Γ€" near "a", not after "z" in German
const auto& coll = std::use_facet<std::collate<char>>(std::locale("de_DE.UTF-8"));
auto lt_de = [&](std::string_view a, std::string_view b) {
return coll.compare(a.data(), a.data() + a.size(),
b.data(), b.data() + b.size()) < 0;
};
// Locale-aware character classification β two-argument form from <locale>
std::locale loc("en_US.UTF-8");
bool alpha = std::isalpha('Γ©', loc); // true with UTF-8 locale; false with classic()
bool space = std::isspace('\t', loc); // true
bool digit = std::isdigit('5', loc); // trueCharacter Classification (<cctype>)
#include <cctype>
// Single-argument versions use the current C locale.
// CRITICAL: argument must be representable as unsigned char, or be EOF.
// Passing a negative plain char is undefined behavior on most platforms (signed char).
// WRONG β UB when char is signed and c > 127
char c = '\xE9';
std::isalpha(c);
// CORRECT β cast first
std::isalpha(static_cast<unsigned char>(c));
// Helper that encapsulates the cast
auto classify = [](char c, auto fn) {
return fn(static_cast<unsigned char>(c));
};
classify('a', std::isalpha); // true
classify('5', std::isdigit); // true
classify('!', std::ispunct); // true
classify(' ', std::isspace); // true
classify('A', std::isupper); // true
classify('f', std::isxdigit); // true
// Case conversion β return int; cast back to char explicitly
char upper = static_cast<char>(std::toupper(static_cast<unsigned char>('a'))); // 'A'
char lower = static_cast<char>(std::tolower(static_cast<unsigned char>('Z'))); // 'z'Best Practices
Default to from_chars/to_chars at data boundaries. Parsing user input or external data benefits from no-exception error reporting, strict whitespace rejection, and locale independence. Reserve stoi/stod for internal code where exception-on-error semantics are acceptable.
Guard float locale sensitivity explicitly. If your application imbues a non-C locale (common in user-facing apps), stod("3.14") may fail or silently misparse in locales where . is not the decimal separator. Either parse floats before changing the locale, or always use from_chars for floats.
Check from_chars float compiler support. Integer from_chars works everywhere since C++17, but floating-point support arrived in MSVC 2019 16.4, GCC 11, and Clang 14. Feature-test with __cpp_lib_to_chars >= 201611L (integer) and >= 201907L (float).
Prefer std::format over to_string for floats. to_string(3.14) produces "3.140000" β always six decimal places, equivalent to %f. std::format("{:.2f}", 3.14) gives "3.14". For shortest round-trip, std::format("{}", 3.14) (C++20) or to_chars without precision (C++17) are the right tools.
Set the global locale once, before threads start. std::locale::global() is a process-wide operation that affects all locale-dependent operations (including the C setlocale). Calling it after worker threads are running is a data race.
Common Pitfalls
from_chars rejects leading whitespace. Unlike strtol and sscanf, a leading space causes ec == errc::invalid_argument. Strip whitespace before calling, or check for it explicitly.
stoi silently accepts partial input. std::stoi("42abc") returns 42 without any indication that input was not fully consumed. Always pass a non-null pos and verify it equals str.size() when full consumption is required.
numpunct::grouping() values are group sizes, not ASCII digits. The returned std::string contains raw byte values: "\3" means groups of 3, not the character '3' (ASCII 51). Interpreting it as a human-readable string is wrong.
to_string(true) returns "1". There is no to_string overload for bool that produces "true"/"false". Use std::format("{}", true) (C++20) or an explicit ternary.
Named locale construction can throw. std::locale("de_DE.UTF-8") throws std::runtime_error if that locale is not installed on the system. Wrap in try/catch or verify availability before using in portable code.
stoi, stof, stod family
The stoi/stof/stod family wraps the C strtoX functions behind a C++ exception interface. Each function accepts an optional pos out-parameter that records how many characters were consumed, and an optional base for integer variants. Internally they delegate to strtol, strtof, strtod β all of which honor the current C locale for decimal-point detection.
#include <string>
#include <stdexcept> // C++11
// Full integer family
int i = std::stoi ("42");
long l = std::stol ("42");
long long ll = std::stoll ("42");
unsigned long ul = std::stoul ("42");
unsigned long long ull = std::stoull("42");
// Arbitrary base β prefix "0x" or "0" is recognised automatically when base==0
int auto_base = std::stoi("0xFF", nullptr, 0); // 255, hex detected
int oct_auto = std::stoi("0755", nullptr, 0); // 493, octal detected
int hex_exp = std::stoi("0xFF", nullptr, 16); // 255, base explicit
// Detect trailing non-digit characters via pos
std::size_t consumed;
int partial = std::stoi("42abc", &consumed); // partial==42, consumed==2
// Always verify when full-string parse is required:
std::string input = "42abc";
if (consumed != input.size()) { /* trailing garbage */ }
// Float family β decimal point follows the current C locale
float f = std::stof ("3.14");
double d = std::stod ("3.14159265358979");
long double ld = std::stold("3.14159265358979323846L");
// Exception behaviour β both exception types derive from std::logic_error
try {
std::stoi("not-a-number"); // throws std::invalid_argument
std::stoi("9999999999999999999"); // throws std::out_of_range
} catch (const std::invalid_argument& e) { /* no conversion could be performed */ }
catch (const std::out_of_range& e) { /* representable range exceeded */ }Caveat: stod is locale-sensitive for the decimal separator. In a process that has called setlocale(LC_NUMERIC, "de_DE"), the decimal separator is ','; stod("3.14") may throw or produce 3 with .14 as trailing garbage. Use from_chars (C++17) when locale independence is required.
std::from_chars (C++17) β locale-independent parsing
std::from_chars is the fastest and safest portable parser: no allocation, no exceptions, no locale dependency, no whitespace skipping. It is the standard-library answer to sscanf and strtod for performance-critical and safety-critical code paths.
#include <charconv> // C++17
#include <system_error>
#include <string_view>
// Basic integer parse β returns from_chars_result {ptr, ec}
std::string_view sv = " 42"; // note leading space
int n = 0;
auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), n);
// ec == std::errc::invalid_argument β from_chars does NOT skip whitespace
// Strip whitespace manually, then parse
sv.remove_prefix(sv.find_first_not_of(' '));
std::from_chars(sv.data(), sv.data() + sv.size(), n); // ec == errc{}, n == 42
// Hexadecimal and binary integers
int hex_n, bin_n;
std::from_chars("0xff" + 2, "0xff" + 4, hex_n, 16); // 255 (caller strips "0x")
std::from_chars("1010", "1010" + 4, bin_n, 2); // 10
// Full-string consume check
std::string_view full = "1234";
int v;
auto res = std::from_chars(full.data(), full.data() + full.size(), v);
bool fully_consumed = (res.ec == std::errc{}) && (res.ptr == full.data() + full.size());
// Floating-point β C++17 standard; requires GCC 11+, Clang 14+, MSVC 2019 16.4+
// Feature-test macro: __cpp_lib_to_chars >= 201907L
double d;
std::from_chars("3.14159265358979", "3.14159265358979" + 16, d);
// Result is the unique double nearest the mathematical value β round-trip safe
// Format flags via std::chars_format
std::from_chars("3.14e2", "3.14e2" + 6, d, std::chars_format::scientific); // 314.0
std::from_chars("1.cp+2", "1.cp+2" + 6, d, std::chars_format::hex); // hexfloat
std::from_chars("3.14", "3.14" + 4, d, std::chars_format::fixed); // 3.14
// Typical parser wrapper with structured binding
bool parse_int(std::string_view s, int& out) {
auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), out);
return ec == std::errc{} && ptr == s.data() + s.size();
}from_chars never writes to value on failure, so initialise the variable before calling if you want to inspect a partial result after an error.
std::to_chars (C++17) β locale-independent formatting
std::to_chars writes directly into a caller-supplied buffer with no allocation and no locale influence, making it the preferred serialisation primitive for protocols and file formats. It returns a to_chars_result {ptr, ec} where ptr is one past the last written character on success, and ec == std::errc::value_too_large on buffer overflow.
#include <charconv> // C++17
#include <array>
#include <string>
// Integer to char buffer
char buf[32];
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), 42);
// On success: ec == errc{}, buf[0..end) == "42"
std::string_view sv{buf, end}; // zero-copy view into the buffer
// Hexadecimal (lowercase by specification)
std::to_chars(buf, buf + sizeof(buf), 255, 16); // "ff"
std::to_chars(buf, buf + sizeof(buf), 255, 2); // "11111111"
// Float β shortest decimal that round-trips back to the exact same double
std::to_chars(buf, buf + sizeof(buf), 0.1); // "0.1"
std::to_chars(buf, buf + sizeof(buf), 3.14159265358979, std::chars_format::fixed, 4); // "3.1416"
std::to_chars(buf, buf + sizeof(buf), 1e100, std::chars_format::scientific); // "1e+100"
std::to_chars(buf, buf + sizeof(buf), 3.14, std::chars_format::hex); // hexfloat
// Buffer-size safety β check for overflow
std::array<char, 4> small;
auto [p, e] = std::to_chars(small.data(), small.data() + small.size(), 99999);
if (e == std::errc::value_too_large) { /* buffer too small; no partial output written */ }
// Zero-allocation string conversion helper
std::string int_to_string(int val) {
char buf[12]; // enough for any 32-bit int including sign
auto [end, ec] = std::to_chars(buf, buf + sizeof(buf), val);
return std::string{buf, end};
}Unlike sprintf, to_chars never null-terminates the buffer; use the returned ptr to delimit the output, or append '\0' manually when a C-string is required.
wstring_convert and encoding
std::wstring_convert (C++11) bridged narrow and wide string encodings but was deprecated in C++17 because it throws std::range_error on conversion failure with no recoverable state, it is not thread-safe by default, and the underlying std::codecvt facets were deprecated simultaneously. Avoid it in new code; prefer platform APIs or third-party libraries such as ICU or simdutf.
// C++11 β deprecated in C++17, removed discussion in C++26
#include <locale>
#include <codecvt>
#include <string>
// Convert UTF-8 <-> UTF-32
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> conv32;
std::u32string u32 = conv32.from_bytes("hello"); // UTF-8 β UTF-32
std::string u8 = conv32.to_bytes(u32); // UTF-32 β UTF-8
// Convert UTF-8 <-> UTF-16
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> conv16;
std::u16string u16 = conv16.from_bytes("hello"); // UTF-8 β UTF-16
std::string back = conv16.to_bytes(u16); // UTF-16 β UTF-8
// PREFERRED C++20 alternative: use std::u8string + platform-specific transcoding
// or a library. The standard has no replacement yet.// Minimal portable UTF-8 β UTF-32 codepoint iteration (no deprecated APIs)
// C++20 β manual decode, no library dependency
#include <string_view>
#include <cstdint>
uint32_t decode_utf8(std::string_view& sv) {
unsigned char c = static_cast<unsigned char>(sv[0]);
uint32_t cp;
int bytes;
if (c < 0x80) { cp = c; bytes = 1; }
else if (c < 0xE0) { cp = c & 0x1F; bytes = 2; }
else if (c < 0xF0) { cp = c & 0x0F; bytes = 3; }
else { cp = c & 0x07; bytes = 4; }
for (int i = 1; i < bytes; ++i)
cp = (cp << 6) | (static_cast<unsigned char>(sv[i]) & 0x3F);
sv.remove_prefix(bytes);
return cp;
}Locale facets: num_get and num_put
num_get and num_put are the facets that std::istream and std::ostream call internally for locale-aware numeric I/O. Accessing them directly lets you parse or format numbers using locale rules without constructing a full stream.
#include <locale>
#include <sstream> // C++11
// num_put: format an integer using locale rules into an ostreambuf_iterator
std::locale loc("en_US.UTF-8");
std::ostringstream oss;
oss.imbue(loc);
const auto& np = std::use_facet<std::num_put<char>>(loc);
np.put(std::ostreambuf_iterator<char>(oss), oss, ' ', static_cast<long>(1234567));
// oss.str() == "1,234,567" β thousands separator inserted by facet
// num_get: parse a locale-formatted number from an istreambuf_iterator
std::istringstream iss("1,234,567");
iss.imbue(loc);
const auto& ng = std::use_facet<std::num_get<char>>(loc);
long result = 0;
std::ios_base::iostate err = std::ios_base::goodbit;
ng.get(std::istreambuf_iterator<char>(iss), {}, iss, err, result);
// result == 1234567 if locale correctly groups by thousands
// numpunct β query formatting characters without doing I/O
const auto& punct = std::use_facet<std::numpunct<char>>(loc);
punct.decimal_point(); // '.'
punct.thousands_sep(); // ','
punct.grouping(); // "\3" β raw byte 3, NOT the character '3'
punct.truename(); // "true"
punct.falsename(); // "false"numpunct::grouping() returns raw byte values, not ASCII digit characters. The string "\3" means groups of three digits; "\3\2" means the rightmost group has 3 digits and all remaining groups have 2. Misreading this as a human-readable string is a common bug.
UTF-8/UTF-16/UTF-32 conversion
C++ provides std::u8string (C++20), std::u16string, and std::u32string as type-safe containers for the three Unicode encodings, but provides no standard transcoding functions between them. The deprecated <codecvt> header was the only in-standard option and was removed in C++26. For production code, use ICU, simdutf, or a lightweight header library.
#include <string> // C++20
// Type aliases emphasising encoding
using Utf8String = std::u8string; // char8_t elements, C++20
using Utf16String = std::u16string; // char16_t elements
using Utf32String = std::u32string; // char32_t elements
// Literals β encoding is compile-time enforced, C++20
Utf8String u8 = u8"hello Γ©"; // UTF-8 encoded
Utf16String u16 = u"hello Γ©"; // UTF-16 encoded
Utf32String u32 = U"hello Γ©"; // UTF-32 encoded
// std::string and std::u8string are NOT implicitly convertible in C++20
// std::string s = u8"hello"; // error: cannot convert char8_t* to char*
// Explicit bit-cast workaround (zero-cost):
std::string as_bytes(std::u8string_view u8sv) {
return {reinterpret_cast<const char*>(u8sv.data()), u8sv.size()};
}// simdutf β high-performance, header-available transcoding (not stdlib)
// Shows the idiomatic pattern for production UTF conversion:
// #include <simdutf.h>
// size_t utf16_len = simdutf::utf16_length_from_utf8(u8data, u8len);
// std::u16string out(utf16_len, 0);
// simdutf::convert_utf8_to_utf16le(u8data, u8len,
// reinterpret_cast<char16_t*>(out.data()));
// ICU pattern (common in i18n applications):
// UnicodeString ustr = UnicodeString::fromUTF8(StringPiece(utf8_data, len));
// std::string utf8_out;
// ustr.toUTF8String(utf8_out);There is no portable, non-deprecated, standard-library-only UTF-8 β UTF-16 transcoder in C++20 or earlier. Until the committee standardises a replacement (P2728 and related papers are in progress), the recommended approach is to use simdutf for performance-sensitive code or ICU for full Unicode algorithm support.
See Also
std::formatβ C++20 type-safe, locale-controllable formatting<charconv>β fullfrom_chars/to_charsspecificationstd::ostringstreamβ locale-imbued stream formattingstd::localefacets β complete facet hierarchy:ctype,numpunct,collate,moneypunct,time_put