Skip to content
C++
Library
since C++11
Intermediate

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++11

C++ 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:

  1. <charconv> primitives (C++17) β€” from_chars and to_chars. No allocation, no locale, no exceptions. The fastest portable option; default choice for performance-sensitive or safety-critical parsing.
  2. <string> family (C++11) β€” stoi, stol, stod, to_string. Convenient, throw on error, locale-sensitive for floating-point decimal separators.
  3. <cstdio> functions β€” sscanf, sprintf, atoi. Legacy C API with poor error reporting. Avoid in new code.
  4. std::locale and 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

cpp
// <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)

cpp
#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
cpp
#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)

cpp
#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)

cpp
#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

cpp
#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"));
cpp
#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);  // true

Character Classification (<cctype>)

cpp
#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.

cpp
#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.

cpp
#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.

cpp
#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.

cpp
// 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.
cpp
// 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.

cpp
#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.

cpp
#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()};
}
cpp
// 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