String Conversions
C++ string conversions — to_string, stoi/stof, from_chars, to_chars, and std::format with C++ version discipline and performance trade-offs.
String Conversionssince C++11Converting between numbers and strings via exception-based APIs (C++11), zero-copy buffer APIs (C++17), or formatted output (C++20).
Overview
C++ offers four conversion strategies, each with different allocation, error-handling, and locale semantics:
| API | Allocates | Throws | Locale | Use When |
|---|---|---|---|---|
std::to_string / std::stoi | Yes | Yes | Yes | User-facing input or output where exceptions fit your error model |
std::format | Yes | No | No | Flexible formatting (e.g., hex, scientific notation) with fixed precision |
std::from_chars / std::to_chars | No | No | No | High-performance parsing (protocols, files, networking) |
The choice isn't about "better" — it's about matching your constraints: do you have a buffer pre-allocated? Do you accept exceptions? Must parsing be locale-independent?
Number → String
std::to_string (C++11)
The simplest approach, always allocates a new string:
#include <string>
int i = 42;
double d = 3.14159;
std::string s1 = std::to_string(i); // "42"
std::string s2 = std::to_string(d); // "3.141590" (default 6 decimal places)
std::string s3 = std::to_string(0xFF); // "255" (always decimal)Limitation: std::to_string always uses %g format for floats (6 significant digits, not precision), and there's no way to control output format. For precise control, use std::format (C++20) instead.
std::to_chars (C++17)
Zero-allocation alternative for pre-allocated buffers:
#include <charconv>
int value = 42;
char buf[32];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), value);
if (ec == std::errc{}) {
std::string_view result(buf, ptr); // "42", no allocation
std::println("{}", result);
} else {
// ec == std::errc::value_too_large means buffer too small
}For floating-point, specify format and precision:
double pi = 3.14159265358979;
char buf[50];
// Shortest round-trip representation (default)
auto [ptr1, ec1] = std::to_chars(buf, buf + sizeof(buf), pi);
// Fixed-point, 4 decimal places: "3.1416"
auto [ptr2, ec2] = std::to_chars(buf, buf + sizeof(buf), pi,
std::chars_format::fixed, 4);
// Scientific notation, 3 digits: "3.142e+00"
auto [ptr3, ec3] = std::to_chars(buf, buf + sizeof(buf), pi,
std::chars_format::scientific, 3);
// Hex float: "0x1.921fb54442d18p+1"
auto [ptr4, ec4] = std::to_chars(buf, buf + sizeof(buf), pi,
std::chars_format::hex, 8);std::format (C++20)
Flexible formatting with allocation:
#include <format>
int n = 255;
double d = 3.14;
std::string s1 = std::format("{}", n); // "255"
std::string s2 = std::format("{:08b}", n); // "11111111" (binary, padded)
std::string s3 = std::format("{:x}", n); // "ff" (hex)
std::string s4 = std::format("{:.2f}", d); // "3.14" (2 decimal places)
std::string s5 = std::format("{:e}", d); // "3.140000e+00" (scientific)String → Number
std::stoi / std::stof / std::stod (C++11)
Exception-based, locale-sensitive parsing:
#include <string>
try {
int n1 = std::stoi("42"); // 42
int n2 = std::stoi(" -42"); // -42 (leading whitespace)
int n3 = std::stoi("42abc"); // 42 (stops at first non-digit)
int n4 = std::stoi("0xFF", nullptr, 16); // 255 (hex)
int n5 = std::stoi("1010", nullptr, 2); // 10 (binary)
double d1 = std::stod("3.14"); // 3.14
double d2 = std::stod("1.23e-4"); // 0.000123 (scientific)
size_t idx;
int n6 = std::stoi("42xyz", &idx); // idx=2 (chars consumed)
} catch (const std::invalid_argument&) {
std::println("bad format");
} catch (const std::out_of_range&) {
std::println("value out of range");
}Caveats:
- Locale-sensitive: in German locale,
"3,14"is valid (comma as decimal). - Always allocates internally (creates a
std::stringfor parsing). - Whitespace-tolerant: leading/trailing whitespace is ignored.
std::from_chars (C++17)
Zero-exception, zero-allocation alternative:
#include <charconv>
#include <string_view>
// Integer parsing
std::string_view text = "42";
int value;
auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), value);
if (ec == std::errc{}) {
std::println("parsed: {}", value); // 42
std::println("remaining: {}", std::string_view(ptr, text.data() + text.size()));
} else if (ec == std::errc::invalid_argument) {
std::println("not a number");
} else if (ec == std::errc::result_out_of_range) {
std::println("overflow");
}
// Floating-point parsing
std::string_view ftext = "3.14159";
double dval;
auto [fptr, fec] = std::from_chars(ftext.data(), ftext.data() + ftext.size(), dval);
// Different bases
auto [bptr, bec] = std::from_chars(text.data(), text.data() + text.size(), value, 16); // hexAdvantages:
- Locale-independent (always uses C locale).
- No exceptions: errors returned via
std::errc. - No allocation: writes directly to reference parameter.
- Pointer return tells you where parsing stopped (critical for streaming/chunked input).
Best Practices
Choose by context, not by fashion
- User input (CLI, form):
std::stoi/std::stod— exceptions signal user error naturally, and you expect small numbers. - Protocol/file parsing:
std::from_chars— no allocation, no locale, streaming-friendly. - Output formatting:
std::format(C++20) for control,std::to_string(C++11) for simplicity. - High-frequency loops:
std::to_chars/std::from_charsto avoid allocation and exception overhead.
Streaming / chained parsing
For CSV or protocol data, std::from_chars shines because the pointer return enables resumable parsing:
#include <charconv>
std::string_view csv = "10,20,30";
const char* p = csv.data();
const char* end = p + csv.size();
std::vector<int> values;
while (p < end) {
int val;
auto [next, ec] = std::from_chars(p, end, val);
if (ec != std::errc{}) break;
values.push_back(val);
p = next;
if (p < end && *p == ',') ++p; // skip comma
}
// values = {10, 20, 30}Error handling patterns
Exception-based (C++11):
try {
int n = std::stoi(input);
process(n);
} catch (...) {
log_error();
}Return-code-based (C++17):
int n;
auto [ptr, ec] = std::from_chars(input.data(), input.data() + input.size(), n);
if (ec == std::errc{}) {
process(n);
} else {
log_error(std::make_error_code(ec).message());
}Performance and Allocation
std::to_chars / std::from_chars win decisively on allocation:
std::to_string(1000000)allocates once, copies into the returned string.std::to_charswrites directly to your buffer — zero allocation.std::stoi("1000000")allocates a temporary string internally.std::from_charsparses directly from the input pointer — zero allocation.
For bulk conversions or tight loops, the difference is measurable. For one-off conversions, it's negligible.
Locale Independence
std::stoi/std::stod are locale-sensitive:
std::locale loc("de_DE"); // German: comma is decimal separator
std::istringstream iss("3,14");
iss.imbue(loc);
double d;
iss >> d; // 3.14std::from_chars is always C-locale (period as decimal):
std::string_view text = "3,14";
double d;
auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), d);
// d = 3.0, "14" unparsed — comma is not recognized as decimalFor file formats, protocols, or system configuration (JSON, CSV, HTTP headers), always use std::from_chars (C++17) to avoid locale surprises.
Common Pitfalls
Buffer overflow with std::to_chars
Always provide enough space — for int, 11 characters + null is safe; for double, 24+ is prudent:
char buf[11];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), -2147483648); // fitsPrecision confusion in std::to_string
std::to_string uses %g format (6 significant digits, not decimal places). For 3.141592653, you get "3.14159". Use std::format (C++20) if precision matters:
double pi = 3.141592653;
std::string s1 = std::to_string(pi); // "3.141593" (6 sig figs)
std::string s2 = std::format("{:.10f}", pi); // "3.1415926530" (10 decimals)from_chars doesn't skip whitespace
Unlike std::stoi, std::from_chars stops at the first non-digit:
std::string_view text = " 42";
int n;
auto [ptr, ec] = std::from_chars(text.data(), text.data() + text.size(), n);
// ec == std::errc::invalid_argument — space is not a digitSkip whitespace manually if needed.
Locale traps with stoi in international contexts
If you're parsing user input from a German locale expecting "3,14", std::stod respects the locale and returns 3.14. But if that same input reaches your backend as JSON or a CSV file, std::from_chars will fail. Normalize input early or use the right API for the source.
std::to_string and std::to_wstring — Precision Caveats
std::to_string is the quickest path from a number to an std::string, but its formatting is fixed and often surprising for floating-point values. Internally it is equivalent to calling std::sprintf with %g, which produces at most 6 significant digits and may use scientific notation for very large or very small values.
// C++11
#include <string>
// Integer variants — straightforward
std::string a = std::to_string(0); // "0"
std::string b = std::to_string(-2147483648); // "-2147483648"
std::string c = std::to_string(0xDEAD); // "57005" (always decimal)
// Unsigned and 64-bit
unsigned long long big = 18446744073709551615ULL;
std::string d = std::to_string(big); // "18446744073709551615"
// Float/double — 6 significant digits, trailing zeros stripped
std::string e = std::to_string(3.14f); // "3.140000" — note: float, not double
std::string f = std::to_string(1e-8); // "0.000000" — precision lost!
std::string g = std::to_string(1.23456789); // "1.234568" — rounds to 6 sig figsWide-string variant std::to_wstring mirrors std::to_string exactly but returns std::wstring. Both functions are locale-sensitive: the decimal point character and digit grouping follow the current global locale, which makes them unsuitable for serialization formats where a stable representation is required. Use std::format or std::to_chars instead whenever locale independence matters.
// C++11
#include <string>
std::wstring ws = std::to_wstring(255); // L"255"
std::wstring wd = std::to_wstring(2.718); // L"2.718000"Contrast with std::format, which gives you explicit precision control without locale influence:
// C++20
#include <format>
double pi = 3.141592653589793;
std::string s1 = std::to_string(pi); // "3.141593" — 6 sig figs, locale-aware
std::string s2 = std::format("{}", pi); // "3.141592653589793" — shortest round-trip
std::string s3 = std::format("{:.4f}", pi); // "3.1416" — 4 decimal places, locale-free
std::string s4 = std::format("{:+.2e}", pi); // "+3.14e+00" — signed scientific notationstd::format for String Conversion (C++20)
std::format is the modern, type-safe replacement for both sprintf and std::ostringstream when you need to produce a formatted string. It is locale-independent by default, throws std::format_error only for malformed format strings (not for value overflow), and supports a rich mini-language for alignment, padding, sign, base, precision, and type.
// C++20
#include <format>
// Integers — base, width, fill, sign
std::string s1 = std::format("{:d}", 255); // "255" decimal
std::string s2 = std::format("{:x}", 255); // "ff" lowercase hex
std::string s3 = std::format("{:X}", 255); // "FF" uppercase hex
std::string s4 = std::format("{:#x}", 255); // "0xff" with prefix
std::string s5 = std::format("{:08b}", 255); // "11111111" binary, zero-padded
std::string s6 = std::format("{:>10}", 255); // " 255" right-aligned
// Floats — precision, notation
std::string s7 = std::format("{:.6f}", 3.14); // "3.140000"
std::string s8 = std::format("{:.2e}", 12345.678); // "1.23e+04"
std::string s9 = std::format("{:g}", 0.00001); // "1e-05" shorteststd::format avoids the three classic pitfalls of sprintf: it is type-safe (the format string is checked against argument types at compile time), it does not write into a caller-supplied buffer (so there is no overflow), and it does not silently truncate. For performance-critical paths that must avoid heap allocation entirely, prefer std::format_to with a pre-allocated buffer or std::to_chars.
// C++20
#include <format>
#include <vector>
// format_to — write into an existing output iterator, no extra allocation
std::string result;
result.resize(64);
auto it = std::format_to(result.begin(), "x={:.3f}", 2.71828);
result.resize(it - result.begin()); // trim to actual length
// result == "x=2.718"
// format_to with a back_inserter for dynamic growth
std::string out;
std::format_to(std::back_inserter(out), "n={}", 42);C++23 adds std::print and std::println, which combine formatting and I/O into a single call:
// C++23
#include <print>
std::println("value = {:.2f}", 3.14159); // formats + writes to stdout + newline
std::print(stderr, "error {}\n", 404); // target any FILE*Wide String and Unicode Conversion
Converting between narrow (std::string) and wide (std::wstring) strings has never had a clean, stable C++ standard solution, and the situation changed significantly in C++17. The <codecvt> header and std::wstring_convert were deprecated in C++17 and removed in C++26; do not use them in new code.
// C++11/14 — DEPRECATED in C++17, removed in C++26. Shown only for recognition.
#include <codecvt>
#include <locale>
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>> conv;
std::wstring wide = conv.from_bytes("hello"); // narrow UTF-8 → wide UTF-16
std::string narrow = conv.to_bytes(wide); // wide UTF-16 → narrow UTF-8
// Do NOT use this in new code.The idiomatic replacement depends on your platform and requirements. For Windows, MultiByteToWideChar / WideCharToMultiByte are reliable. For cross-platform code, the lightest option that requires no external library is the POSIX mbstowcs / wcstombs family (locale-dependent), but for correctness and portability, a dedicated library is the practical answer.
// C++17 — platform-neutral option: use std::filesystem::path as an intermediary
// filesystem::path stores paths in a portable encoding and converts on construction
#include <filesystem>
std::filesystem::path p = u8"résumé.txt"; // UTF-8 source literal
std::wstring wide_path = p.wstring(); // platform-native wide string
std::string narrow_path = p.string(); // platform-native narrow string
std::u8string u8_path = p.u8string(); // guaranteed UTF-8C++20 introduced char8_t and std::u8string to make UTF-8 source literals unambiguous, but the standard library does not yet provide a char8_t ↔ char32_t conversion function. Until such facilities arrive (proposals are active for C++26), libraries such as simdutf, ICU, or utfcpp remain the recommended choice for production Unicode conversion work.
// C++20 — char8_t for unambiguous UTF-8 literals
#include <string>
std::u8string s = u8"こんにちは"; // UTF-8 encoded
std::string raw(s.begin(), s.end()); // reinterpret as raw bytes (no conversion)
// For actual re-encoding (e.g., UTF-8 → UTF-32), use a dedicated library.Error Handling in Conversions
The two conversion families differ fundamentally in how they signal errors: std::stoi/std::stof throw C++ exceptions, while std::from_chars uses error codes — and this difference drives which API fits your architecture.
Exception-based APIs (stoi, stol, stoll, stoul, stof, stod, stold) throw two exception types: std::invalid_argument when the string contains no parseable number, and std::out_of_range when the value exceeds the target type's representable range. They are convenient when conversion failure is genuinely exceptional and the call site already participates in exception handling.
// C++11
#include <string>
#include <stdexcept>
auto safe_parse = [](const std::string& s) -> std::optional<int> {
try {
size_t pos = 0;
int val = std::stoi(s, &pos);
if (pos != s.size()) return std::nullopt; // trailing garbage
return val;
} catch (const std::invalid_argument&) {
return std::nullopt; // not a number at all
} catch (const std::out_of_range&) {
return std::nullopt; // e.g., "9999999999" for int
}
};std::from_chars uses the std::errc enumeration instead of exceptions, making it suitable for code that parses untrusted or high-volume input where exception overhead would be prohibitive. The two error codes you will encounter are std::errc::invalid_argument (nothing parseable at the start of the range) and std::errc::result_out_of_range (overflow or underflow).
// C++17
#include <charconv>
#include <system_error>
struct ParseResult { bool ok; int value; std::string_view remainder; };
ParseResult parse_int(std::string_view input) {
int value{};
auto [end, ec] = std::from_chars(input.data(), input.data() + input.size(), value);
if (ec == std::errc{}) {
return { true, value, std::string_view(end, input.data() + input.size() - end) };
}
if (ec == std::errc::result_out_of_range) {
// value is unspecified — do not use it
}
return { false, 0, input };
}A subtle but important point: when from_chars returns std::errc::result_out_of_range, the value output parameter is left in an unspecified state — do not read it. By contrast, when stoi throws std::out_of_range, the partially-parsed integer is simply discarded with the exception, so there is no analogous hazard. Choose std::from_chars in performance-sensitive parsing paths; choose stoi where readability and natural exception propagation matter more than throughput.
See Also
<charconv>— Low-level conversion (C++17)<format>— Formatted output (C++20)std::string::substr()— Extract substrings for manual parsingstd::istringstream— Stream-based parsing (locale-aware alternative)std::numbers— Mathematical constants (C++20)