Chapter 09 of 24 | C++

Strings and text

For ordinary owned text, use std::string from <string>. It manages its character storage and can grow as needed. A string literal such as "hello" is a sequence of characters with a terminating null character; it is not itself a std::string.

#include <string>

std::string greeting{"Hello"};
greeting += ", C++";
greeting.push_back('!');
std::cout << greeting << '\n';
std::cout << greeting.size() << '\n';

Access, search, and substrings

String indexes are zero-based. Use size() to learn the number of characters stored and avoid indexing an empty string. find searches for text and returns std::string::npos when nothing is found:

const std::string text{"C++ is practical"};
const auto position = text.find("practical");
if (position != std::string::npos) {
    std::cout << text.substr(position) << '\n';
}

substr(start, count) creates a new string. For performance-sensitive code, consider whether copying is needed; std::string_view can refer to existing character data without owning it, but the referenced storage must outlive the view and must not be invalidated.

Text is not always one byte per character

std::string stores bytes. UTF-8 characters can occupy multiple bytes, so size() counts bytes, not necessarily user-perceived characters. Correct Unicode editing, normalization, and case conversion require more than indexing each byte as if it were a complete character.

For a simple ASCII-only transformation, make its limitation explicit. Do not assume that changing a byte's numeric value correctly changes every language's letter. When reading lines, std::getline preserves spaces within the line.

String operations and their costs

Appending text with += or append modifies the existing string. The implementation may reserve extra capacity so repeated appends are efficient on average, but a growth operation can still allocate new memory. When building a large result from many pieces, estimate the final size and call reserve if that estimate is reliable.

Operations such as substr produce a new owning string, which means they copy the selected characters. This is often the right, safe choice. A std::string_view can avoid the copy when you only need temporary read-only access, but it stores a pointer and a length rather than owning data. Never return or store a view beyond the lifetime of the string it refers to.

#include <string_view>

bool starts_with_cpp(std::string_view text)
{
    return text.starts_with("C++"); // C++20 and later
}

String views do not guarantee null termination at their end. Do not pass view.data() to an API that expects a null-terminated string unless you can guarantee that the viewed range is terminated there. Use the view's size-aware operations or create an owning string when crossing such an interface.

Parsing and validating text

Text from a user or file is untrusted input. Check that a parsed value is present, in range, and followed only by allowed characters. The standard library provides numeric conversion facilities, but each has specific error behavior; read the documentation and handle invalid and out-of-range inputs explicitly. For complex formats, use a parser designed for that format instead of splitting text on a delimiter and assuming fields cannot contain that delimiter.

Whitespace is data unless your program deliberately treats it otherwise. Formatted extraction skips leading whitespace and reads one token, while getline reads through a line boundary. Trimming leading or trailing whitespace is a separate transformation; do it only if the input format calls for it.

Unicode and user-visible text

UTF-8 encodes a Unicode code point using one to four bytes. A visible symbol can contain multiple code points, and a user-perceived character may contain multiple combined code points. Consequently, byte indexing is not character indexing, and character count is not necessarily the same as the number of displayed glyphs. Correct cursor movement, case conversion, and text normalization need Unicode-aware libraries and a clearly selected policy.

For protocol data and file formats, specify the encoding and validate it at the boundary. Do not treat arbitrary bytes as valid UTF-8 just because they fit inside a std::string. The string type stores bytes; it does not enforce an encoding.

Practice

Key idea: use owning strings for owned text, views for temporary non-owning access, and be explicit about encoding and lifetime.

Code units, code points, and grapheme clusters

Text has several layers. A code unit is the smallest storage unit of an encoding; a Unicode code point is an abstract numbered character; a grapheme cluster is a sequence a user may perceive as one character. UTF-8 stores code units in eight-bit units and represents a code point with one or more units. A displayed symbol can also consist of multiple code points, such as a base letter followed by a combining mark.

std::string exposes bytes and byte-oriented operations. It does not decode UTF-8, normalize equivalent spellings, perform locale-aware case folding, or measure user-visible characters. std::u8string uses char8_t code units in C++20 and later, but it is not automatically interchangeable with std::string. A conversion between them should state the encoding and validate the input rather than reinterpret bytes without a policy.

Views, ownership, and mutation

A string view is a borrowed range. Constructing one from a temporary string can leave it dangling immediately after the full expression ends. Mutating an owning string can invalidate views into its character storage if the operation reallocates or changes the relevant characters. The type communicates non-ownership, but the programmer must still enforce the lifetime relationship.

At an API boundary, decide whether the function needs to own the text, observe it only during the call, or retain it. Use std::string for ownership, std::string_view for a read-only call-scoped view, and an owning copy if the callee needs to store the text. Do not retain a view supplied by a caller unless the API explicitly establishes an adequate lifetime contract.

Text normalization and security

Two visually identical Unicode strings can have different code-point sequences. This affects equality, search, identifiers, and security-sensitive comparisons. Normalization, case folding, bidirectional controls, and confusable characters require a Unicode-aware library and a policy appropriate to the application. Trimming or lowercasing bytes is not a substitute.

When accepting text from files or networks, define whether malformed encoding is rejected, repaired, or preserved as raw bytes. Silent replacement can change identifiers or content. For security-sensitive identifiers, normalize consistently at the boundary and compare according to the specification; do not make ad hoc transformations at different layers.

Advanced exercise

  1. Write a byte-counting function and explain why it cannot claim to count displayed characters.
  2. Construct a string view from owned storage, mutate or destroy the owner, and use sanitizers or careful reasoning to identify the lifetime bug without relying on undefined execution.
  3. Specify an encoding, normalization, and invalid-input policy for a small text-file format.
0% read