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

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

Converting 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:

APIAllocatesThrowsLocaleUse When
std::to_string / std::stoiYesYesYesUser-facing input or output where exceptions fit your error model
std::formatYesNoNoFlexible formatting (e.g., hex, scientific notation) with fixed precision
std::from_chars / std::to_charsNoNoNoHigh-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:

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

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

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

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

cpp
#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::string for parsing).
  • Whitespace-tolerant: leading/trailing whitespace is ignored.

std::from_chars (C++17)

Zero-exception, zero-allocation alternative:

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

Advantages:

  • 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_chars to avoid allocation and exception overhead.

Streaming / chained parsing

For CSV or protocol data, std::from_chars shines because the pointer return enables resumable parsing:

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

cpp
try {
    int n = std::stoi(input);
    process(n);
} catch (...) {
    log_error();
}

Return-code-based (C++17):

cpp
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_chars writes directly to your buffer — zero allocation.
  • std::stoi("1000000") allocates a temporary string internally.
  • std::from_chars parses 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:

cpp
std::locale loc("de_DE");  // German: comma is decimal separator
std::istringstream iss("3,14");
iss.imbue(loc);
double d;
iss >> d;  // 3.14

std::from_chars is always C-locale (period as decimal):

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

For 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:

cpp
char buf[11];
auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), -2147483648);  // fits

Precision 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:

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

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

Skip 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.

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

Wide-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.

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

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

std::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.

cpp
// 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"    shortest

std::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.

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

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

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

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

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

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

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

cpp
// 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 parsing
  • std::istringstream — Stream-based parsing (locale-aware alternative)
  • std::numbers — Mathematical constants (C++20)