Chapter 9: Text, Unicode, and Formatting
This chapter tours modern C++ text handling. You will meet std::string and std::string_view, learn what Unicode means for your code, format values with std::format, respect users through locale-aware choices, and parse text without surprises. The goal is simple; write programs that handle names, numbers, and messages correctly for everyone.
std::string and std::string_view
std::string owns a mutable byte buffer; it allocates and stores text. std::string_view is a non-owning window over a character sequence (think borrowing rather than owning). Use a view when you only need to read, and a string when you must store or modify. Views are cheap to copy; strings are cheap to move.
Choosing which to use
Prefer std::string_view for parameters when the callee will not keep the data. Prefer std::string for return values when the caller needs an owning result. This keeps lifetimes clear and avoids accidental dangling references.
Common operations and pitfalls
Substrings from std::string copy by default; substrings as std::string_view do not copy. A view can dangle if the referenced buffer goes out of scope. Converting a view to a string materializes a new buffer; this is often the right trade when you need to store or modify.
#include <string>
#include <string_view>
void greet(std::string_view who) {
// uses the caller's storage; no allocation
// safe only while 'who' remains valid
}
std::string shout(std::string_view s) {
std::string owned(s);
for (char& c : owned) c = static_cast(std::toupper(static_cast(c)));
return owned;
}
std::string_view in APIs; store as std::string when you need to keep a copy. This pattern balances performance and safety.
Small string optimization and performance notes
Most standard library implementations keep short strings inside the object; this avoids heap traffic for tiny texts. Do not depend on the exact threshold. Treat it as an implementation detail that often helps but might vary.
Character types and Unicode basics
Text is bytes with meaning. C++ offers multiple character types to represent code units. Unicode assigns each character a code point; encodings such as UTF-8 and UTF-16 map code points to bytes. Always know which encoding your program reads and writes.
Character and string types at a glance
The following table summarizes the main options. Code unit size and encoding expectations differ; choose the pair that matches your I/O needs.
| Type | Code unit | Typical container | Notes |
char | 1 byte | std::string | Bytes; often UTF-8 by convention; not guaranteed |
char8_t | 1 byte | std::u8string | Intended for UTF-8 code units |
char16_t | 2 bytes | std::u16string | UTF-16 code units; surrogate pairs for some code points |
char32_t | 4 bytes | std::u32string | UTF-32 code units; one unit per code point |
wchar_t | 2 or 4 bytes | std::wstring | Implementation defined; avoid for portable Unicode |
UTF-8 as a practical default
UTF-8 is compact for ASCII; it is widely supported across platforms and protocols. Store text as UTF-8 where possible. When you must interoperate with system APIs that use UTF-16 or UTF-32, convert at the boundaries to keep the core simple.
Graphemes, code points, and user expectations
What users see as one character might be multiple code points (for example an emoji plus a skin tone modifier). Simple indexing by code unit can split a visible character. If you need user-visible operations, use a Unicode-aware library that understands grapheme clusters.
Formatting output
std::format prints values with Python-style format strings. Place fields in braces like {…}; add alignment, width, precision, and type specifiers inside the braces. The function is safe and convenient; it avoids iostream quirks and printf hazards.
The format mini-language
A field looks like {:<10.2f} for left alignment, width 10, precision 2, floating style. You can name arguments or use automatic indexing. The same rules apply to std::format_to when writing into existing buffers.
#include <format>
#include <string>
std::string s = std::format("Hello {}, pi≈{:.3f}", "Ada", 3.14159);
// "Hello Ada, pi≈3.142"
std::string t = std::format("|{:>8}|{:^8}|{:<8}|", "R", "G", "B");
// right, centered, left
Custom formatting
Provide a specialization of std::formatter<T> with a parse function for options and a format function to render. This lets your types work naturally inside format strings alongside standard types.
#include <format>
struct point { int x; int y; };
template<>
struct std::formatter<point> {
constexpr auto parse(std::format_parse_context& ctx) { return ctx.begin(); }
auto format(const point& p, std::format_context& ctx) const {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
std::string out = std::format("p={}", point{3,4});
Locale and chrono support
std::format can format time points using {:%Y-%m-%d %H:%M:%S}. By default formatting is locale independent for punctuation; some implementations and newer standards offer overloads that take a std::locale for locale-aware results. Check your toolchain and use explicit locale overloads when you need cultural formatting.
std::format over printf for type safety; prefer it over stream manipulators for clarity and composability.
Locales and cultural correctness
Locale controls language, collation, number punctuation, and date rules. Hard-coding separators and names can confuse users. Respect the active locale when presenting user-facing text and when parsing user input that follows local rules.
Applying std::locale to streams
Imbue a stream with a locale to affect extraction and insertion. This can change decimal separators, thousands separators, and day or month names in formatted output.
#include <locale>
#include <iostream>
int main() {
std::locale loc("fr_FR.UTF-8");
std::cout.imbue(loc);
std::cout << 1234567.89 << "\n";
}
Sorting and case rules differ by language
String comparison by raw code units is not user ordering. Collation depends on language rules; case folding is language sensitive. For human sorts and searches, use a library that implements locale collation rather than naive comparisons.
Use boundary conversions instead of global settings
Keep the core of your program in a stable internal form (often UTF-8 and C locale). Convert and format at the boundaries where you talk to users or systems. This reduces surprises while still delivering culturally correct I/O at the edges.
Parsing text safely
Parsing turns text into typed values. Prefer functions that report errors without throwing when speed or control matters. Avoid ad-hoc parsing with unchecked indexing; use views and robust helpers to prevent overreads and partial conversions.
Numbers with std::from_chars
std::from_chars parses integers and floating values from a byte range without allocations. It reports errors via a return code and a pointer to the first unparsed character; this makes it a good fit for tight loops and streaming.
#include <charconv>
#include <string_view>
int parse_port(std::string_view sv) {
int port = 0;
auto* begin = sv.data();
auto* end = sv.data() + sv.size();
auto res = std::from_chars(begin, end, port);
if (res.ec != std::errc() || res.ptr != end) return -1;
return port;
}
Lines and fields with views and ranges
Use std::string_view to slice without copying and use algorithms to scan. For simple CSV-like data where values do not contain quotes or escapes, a careful split by delimiter can be enough; for full CSV rules rely on a proper parser.
#include <string_view>
#include <vector>
std::vector<std::string_view> split(std::string_view s, char delim) {
std::vector<std::string_view> out;
size_t pos = 0;
while (pos < s.size()) {
size_t next = s.find(delim, pos);
if (next == std::string_view::npos) { out.push_back(s.substr(pos)); break; }
out.push_back(s.substr(pos, next - pos));
pos = next + 1;
}
return out;
}
Timestamps and structured input
For date and time, prefer the chrono parsing facilities that mirror strftime patterns. If your standard library lacks a direct parsing function, use a proven library that supports the formats you must accept, or parse with a finite set of checks and clear error paths.
Error reporting with context
Include position information and a short excerpt to help users fix input. Keep messages neutral and specific; show the unexpected token and the rule that failed. This approach speeds up debugging for both programs and people.
#include <stdexcept>
#include <string>
[[noreturn]] void parse_error(std::string_view where, size_t pos, std::string_view why) {
std::string caret(pos, ' ');
throw std::runtime_error(std::string(where) + "\n" + caret + "^\n" + std::string(why));
}