<< Home

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; 
}
💡 Accept 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.

TypeCode unitTypical containerNotes
char1 bytestd::stringBytes; often UTF-8 by convention; not guaranteed
char8_t1 bytestd::u8stringIntended for UTF-8 code units
char16_t2 bytesstd::u16stringUTF-16 code units; surrogate pairs for some code points
char32_t4 bytesstd::u32stringUTF-32 code units; one unit per code point
wchar_t2 or 4 bytesstd::wstringImplementation 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.

⚠️ The standard library does not provide full Unicode algorithms such as normalization or grapheme segmentation. Use a library such as ICU for advanced tasks.

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.

💡 Prefer 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.

⚠️ Never assume ASCII rules fit all languages. Even uppercase and lowercase mappings vary; for example Turkish dotted and dotless I characters break simple assumptions.

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.

💡 Validate first, then convert. Check length limits, allowed characters, and expected separators before calling a conversion routine; fail fast on the first mismatch.

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));
}