Polyglot
Languages C++ strings
C++ § strings

Strings

C++ provides several string facilities. The principal owning-string type is std::string, an owning, contiguous, mutable sequence of char. C++17 introduced std::string_view as a non-owning view. The C-string interface from <cstring> remains available for compatibility with C and with code that operates on null-terminated buffers. Formatting has evolved through three generations: the iostream insertion operators (<<), the printf-family C functions, and the modern std::format (C++20) plus <print> (C++23) interface that supersedes both.

The string types are not a separate kind of object in the language: std::string is an ordinary class type that happens to be ubiquitous. The discipline of working with strings is therefore the discipline of working with class types: knowing the cost of operations, the reference-vs-value choice, the lifetime of any underlying buffer, and the encoding of the bytes.

std::string

std::string is a class template instantiation: std::basic_string<char>. It owns a heap-allocated buffer (sometimes inlined for short strings, the small-string optimisation), tracks length explicitly, and provides the conventional set of operations:

#include <string>
#include <iostream>

int main() {
    std::string greeting = "Hello";
    greeting += ", ";
    greeting.append("world!");

    std::cout << greeting << '\n';                  // Hello, world!
    std::cout << "length: " << greeting.size() << '\n';
    std::cout << "first:  " << greeting.front() << '\n';
    std::cout << "find:   " << greeting.find("world") << '\n';
}

The principal members and operations:

OperationEffect
s.size() / s.length()Number of bytes; constant time.
s.empty()Whether the string is empty.
s[i]Element access; no bounds check.
s.at(i)Element access; throws std::out_of_range on miss.
s.front(), s.back()First and last elements.
s.data(), s.c_str()Pointer to the underlying buffer; null-terminated.
s.append(...), s += ...Append to the end.
s.insert(pos, ...)Insert at a position.
s.erase(pos, n)Remove a range.
s.replace(pos, n, ...)Replace a range.
s.substr(pos, n)Returns a copy of a sub-range.
s.find(sub, pos), s.rfind(sub, pos)Search; returns position or std::string::npos.
s.starts_with(sub), s.ends_with(sub)(C++20) Prefix and suffix tests.
s.contains(sub)(C++23) Substring test.
s.compare(other)Three-way comparison.

The string is null-terminated for compatibility with C: s.c_str() returns a pointer to a null-terminated const char buffer. Mutating operations may invalidate previously-returned pointers and iterators.

std::string_view

std::string_view (C++17) is a non-owning view of a contiguous character sequence:

#include <string_view>

void print_uppercase(std::string_view sv) {
    for (char c : sv) std::cout << (char)std::toupper((unsigned char)c);
    std::cout << '\n';
}

print_uppercase("hello");                    // C-string literal
print_uppercase(std::string{"world"});       // owning string
print_uppercase(some_buffer);                // any contiguous char sequence

A string_view carries a pointer and a length; copying one is constant-time and does not allocate. The trade-off is lifetime management: a string_view does not own its data, and using one after the underlying storage is destroyed is undefined behaviour. The construction is the conventional choice for parameter types in functions that read but do not store strings; it accepts string literals, std::string, character arrays, and any buffer-and-length pair without forcing an allocation.

Conversely, string_view is not a sound choice for return types from functions that produce strings, nor for member variables of class types whose lifetime exceeds the lifetime of the source data. The conventional rule:

  • Take std::string_view for read-only string parameters.
  • Take const std::string& only when the function genuinely needs a std::string (for example, to call its members).
  • Take std::string (by value) for parameters the function will store, allowing the caller to move into them.

The construction is the principal mechanism by which generic string-consuming functions in modern C++ avoid forcing allocations on their callers.

C-style strings

C-style strings — const char * to a null-terminated array of char — remain available and are the interoperability bridge to C libraries:

#include <cstring>

const char *cs = "hello";
size_t       len = std::strlen(cs);

char buf[64];
std::strncpy(buf, cs, sizeof buf);

The <cstring> header (the C++ form of <string.h>) declares the C string functions in namespace std. The conventional discipline is to use std::string and std::string_view for new C++ code and to convert at the boundary with C code.

Conversion between the forms:

std::string  s   = "hello";
const char  *cs  = s.c_str();           // string -> C string

const char  *cs2 = "world";
std::string  s2  = cs2;                 // C string -> string (allocates)

std::string_view sv = s;                // string -> string_view (free)
std::string_view sv2 = "literal";       // C string -> string_view (free)

The conversion std::string to std::string_view is implicit and free; the reverse direction allocates and is explicit.

Character types

C++ recognises six character types:

TypeWidthPurpose
charimplementation-defined, typically 8The conventional character type; signedness is implementation-defined.
signed char8A signed 8-bit integer.
unsigned char8An unsigned 8-bit integer; the conventional choice for raw bytes.
wchar_timplementation-defined; 16 (Windows) or 32 (Unix)Wide character.
char8_t8 (C++20)A UTF-8 code unit.
char16_t16A UTF-16 code unit.
char32_t32A UTF-32 code unit.

char is the conventional storage for text. The C++20 introduction of char8_t made UTF-8 string literals (u8"…") yield arrays of char8_t rather than char; this caused some friction with existing code, and many APIs continue to use char for UTF-8 by convention.

String literals

A string literal in double quotes denotes an array of const char of length one greater than the number of characters in the source:

"hello"           // const char[6]
""                // const char[1]

C++ admits several literal prefixes:

PrefixType
(none)const char[N]
Lconst wchar_t[N]
u8const char8_t[N] (C++20; was const char[N] before)
uconst char16_t[N]
Uconst char32_t[N]
R"…"Raw string: backslash escapes are not interpreted

The user-defined-literal suffix s (from std::literals::string_literals) yields a std::string:

using namespace std::string_literals;
auto greeting = "hello"s;            // std::string{"hello"}

The user-defined-literal suffix sv (from std::literals::string_view_literals) yields a std::string_view:

using namespace std::string_view_literals;
auto sv = "world"sv;                 // std::string_view{"world"}

The sv form is the conventional way to construct a string view from a literal without going through the C-string overload of string_view’s constructor (which is constant-time but technically separate).

Raw strings

Raw string literals avoid the need to escape backslashes and quotes:

auto regex = R"(\d{4}-\d{2}-\d{2})";        // \d{4}-\d{2}-\d{2}
auto json  = R"({"key": "value"})";

auto with_paren = R"delim(content with )" inside)delim";

The optional delimiter between R" and ( lets the literal contain the closing )" sequence. Raw strings are conventional for regular expressions, JSON literals, and any string with substantial backslash content.

Formatting

C++ has three formatting interfaces. Each predominates at a particular vintage of code.

Iostream insertion operators

The classic interface uses overloaded operator<< to insert formatted values into streams:

#include <iostream>
#include <iomanip>

int    age = 30;
double pi  = 3.14159;
std::cout << "age: " << age << ", pi: "
          << std::fixed << std::setprecision(2) << pi << '\n';
// age: 30, pi: 3.14

The interface is type-safe — overload resolution selects the right operator<< for each operand — but the formatting state is carried by the stream and is awkward to control. Manipulatorsstd::fixed, std::setw, std::setprecision, std::hex, std::endl — adjust the state for subsequent insertions.

The full treatment of streams is in I/O streams.

printf-family C functions

The C-inherited printf family operates on format strings:

#include <cstdio>

std::printf("age: %d, pi: %.2f\n", age, pi);

The mechanism is concise but type-unsafe: a mismatched format string and argument list is undefined behaviour. Modern compilers warn about mismatches when the format string is a literal (-Wformat); when the format string is dynamic, no compile-time check is possible.

std::format and <print>

C++20 introduced std::format, which combines the conciseness of printf with the type safety of iostreams:

#include <format>

std::string s = std::format("age: {}, pi: {:.2f}\n", age, pi);
std::cout << s;

The format string uses {} placeholders with optional formatting specifications ({:.2f}, {:>10}, {:#x}). Each placeholder is type-checked against the corresponding argument at compile time when the format string is a constexpr. The interface is the C++ inheritor of Python’s str.format and the formatting in Rust’s println!.

C++23 added <print>, which writes formatted output directly:

#include <print>

std::print("age: {}, pi: {:.2f}\n", age, pi);
std::println("done.");

The conventional contemporary advice:

  • Use std::format for string construction.
  • Use std::print / std::println for direct output.
  • Use iostreams for stream-based code that already exists.
  • Reserve printf for C-interoperability code.

Encoding considerations

C++ does not enforce a particular text encoding. The conventions:

  • char strings conventionally hold UTF-8 in modern C++. The C++23 specification adopts UTF-8 as the default source encoding.
  • Wide-character strings (wchar_t) hold whatever the platform’s wide encoding is — UTF-16 on Windows, UTF-32 on Unix. They are not portably interchangeable.
  • For Unicode-aware text processing — case folding, normalisation, segmentation, regex over code points — the C++ standard library is insufficient. Production code uses ICU, Boost.Locale, or the platform’s text APIs.

The combination of std::string for storage and std::string_view for read-only passing, with UTF-8 as the implicit encoding, is the conventional shape of string handling in modern C++.