Strings, Small String Optimization (SSO), string_view & span
std::string has a built-in 15-character pocket inside its stack object. Short strings like 'hello' cost 0 heap allocations! Only when you type the 16th character does it rent a heap buffer across town.
SSO stores short strings directly inside the std::string object's stack frame without calling new/malloc. It achieves this by using a union within the 24-byte or 32-byte layout: when in heap mode, the union stores capacity and heap metadata; when in SSO mode, that same memory space is reinterpreted as a fixed-size char local_buf[16] buffer. On 64-bit GCC, strings up to 15 characters plus a null terminator fit inside the object with zero heap allocation.
Both std::string_view and dynamic std::span<T> are 16-byte structs consisting of a raw pointer (const char* or T*, 8 bytes) and an element count (size_t, 8 bytes). On 64-bit calling conventions (System V and Microsoft x64), 16-byte structs fit directly into two CPU registers (rdi/rsi or rcx/rdx). Passing by const& passes an 8-byte pointer to stack memory, requiring the CPU to perform extra memory dereferences rather than operating directly on register values.
C standard library functions (strlen, printf("%s"), fopen, strcmp) assume strings are null-terminated with \0. A std::string_view created via .substr() represents a half-open window $[ptr, ptr + len)$ within a larger string and does not place a \0 byte after the slice. Passing sv.data() to a C function causes it to read past the intended boundary into adjacent memory until it happens to find a random \0 byte, leading to buffer over-reads and security vulnerabilities.
Interviewer's lens
String handling instantly reveals whether an engineer understands memory boundaries or treats C++ like a managed runtime:
- Textbook trap: Assuming every
std::stringinvokesnew, passingconst std::string&everywhere, and falling into danglingstd::string_viewlifetime traps. - Hardware reality: Heap allocations (
malloc/new) carry kernel allocator lock contention and cache misses. Small strings can live entirely on the stack. - Senior skills expected: How Small String Optimization (SSO) provides a 15-byte internal stack pocket before spilling to the heap, proving SSO with pointer address arithmetic, leveraging
std::string_view(16 bytes:ptr + len) for zero-copy slicing, sidestepping the non-null-terminated C-API bug, and using C++20std::span<T>for contiguous buffer mutation.
Under the hood & memory layout
Think of std::string as a jacket with a built-in pocket:
- The 15-Byte Pocket (SSO Mode): The jacket itself occupies 32 bytes on the stack (GCC libstdc++) or 24 bytes (Clang libc++ / MSVC STL). It has an internal union with an inline buffer (
char local_buf[16]). If your text is 15 characters or fewer (plus '\0'), it slips directly into this built-in stack pocketβzero heap allocation (new), zero cache misses, and instant destructor cleanup. - The Heap Spill (> 15 chars): Once the text reaches 16 bytes, it overflows the pocket. The string allocates an external storage container on the heap, and the stack frame repurposes that union space to store the heap pointer (
char* ptr),size, andcapacity. std::string_viewis a Magnifying Glass (16 Bytes): Owns nothing. Stores just two 64-bit values:const char* ptrandsize_t len. Substrings (sv.substr()) simply slide the magnifying glass via pointer arithmetic in O(1) time with zero heap copies.std::span<T>is a Read/Write Magnifying Glass (16 Bytes): Generalizes non-owning views across any contiguous mutable sequence ofT(std::vector,std::array, or raw C buffers).
Data is stored directly inside char local_buf[16] within the 32-byte std::string stack object. s.data() returns &s.local_buf[0]. No heap allocation (operator new) is invoked; destructor requires 0 deallocations.
Allocates a separate heap buffer new char[capacity + 1]. The 32-byte stack frame stores: char* data_ (8 bytes), size_t size_ (8 bytes), and union capacity/allocated size (8β16 bytes). Destructor calls delete[].
Exact 16-byte struct: const char* data_ (8 bytes) + size_t length_ (8 bytes). sv.substr(pos, count) merely evaluates data_ + pos and min(count, length_ - pos), executing in strictly O(1) time with 0 allocations.
Dynamic extent span occupies 16 bytes: T* data_ (8 bytes) + size_t size_ (8 bytes). Provides bounds-checked or raw array indexing span[i] across std::vector, std::array, and C-style arrays with full read/write support.
Core concepts & syntax traps
Small String Optimization (SSO) & Internal Union Layout
Small String Optimization (SSO) & Internal Union Layout
Calling malloc/new for every string is a major performance bottleneck: it fragments memory and stalls threads on allocator locks. Because most strings (usernames, symbols, error codes) are short, C++ uses Small String Optimization (SSO):
The Dual-Mode Union:
Inside std::string, a union overlaps stack storage with heap metadata:
// Conceptual layout in GCC libstdc++ (64-bit, 32 bytes):
class string {
char* _M_dataplus; // 8B: points to heap OR points inside _M_local_buf
size_t _M_string_length; // 8B: current length
union {
char _M_local_buf[16]; // 16B: stores up to 15 chars + '\0'
size_t _M_allocated_capacity; // 8B: active when spilled to heap
};
};
The Two Operating States:
- Pocket Mode (length <= 15): Characters live inside
_M_local_buf.s.data()points directly into the string's own stack frame. Zero heap allocations occur! - Spill Mode (length >= 16): Allocates an external buffer on the heap.
_M_datapluspoints to that heap memory, and_M_allocated_capacitytracks the buffer size.
Proving SSO with Pointer Arithmetic:
If s.data() falls within the address range [&s, &s + 1), the character buffer resides physically inside the stack object itself!
bool isSSO(const std::string& s) {
const char* ptr = s.data();
const char* start = reinterpret_cast<const char*>(&s);
return ptr >= start && ptr < start + sizeof(s);
}
std::string a = "cat"; // len 3: isSSO(a) == true (stack buffer)
std::string b = "This string exceeds fifteen characters!"; // isSSO(b) == false (heap buffer)Short strings (β€ 15 characters on GCC) live completely on the stack with zero heap allocation and instant cache locality.
std::string_view: 16-Byte Non-Owning Zero-Copy Slicing
std::string_view: 16-Byte Non-Owning Zero-Copy Slicing
Before C++17, slicing a string with s.substr() forced a new heap allocation and byte copy.
std::string_view acts as a lightweight, non-owning window into existing characters:
- Exact 16-byte Footprint: Holds only
const char* ptr_(8 bytes) andsize_t len_(8 bytes). - O(1) Substrings:
sv.substr(offset, count)simply evaluatesptr_ + offsetand adjusts length. Zero heap allocations, zero character copies. - Pass By Value: Always pass
std::string_viewby value, never byconst&. On 64-bit ABIs, 16-byte structs pass directly inside two CPU registers (rsi,rdx), completely avoiding memory indirection.
void parse(std::string_view sv) { // passed by value: 16 bytes in CPU registers
std::string_view token = sv.substr(0, 4); // O(1) zero-copy pointer arithmetic
}Pass std::string_view by value: 16 bytes in CPU registers beats pointer indirection and eliminates all heap allocations.
The 2 Deadly string_view Traps: Dangling & Missing Null Terminator
The 2 Deadly string_view Traps: Dangling & Missing Null Terminator
std::string_view borrows memory without owning it, introducing two high-frequency interview bugs:
Trap 1: Dangling Reference to a Temporary String:
// DEADLY BUG: temporary string destroyed at the semicolon!
std::string_view sv = std::string("user_") + id; // sv points to dead memory!
The temporary std::string dies the instant the line finishes executing. The string_view now points to garbage heap memory.
Trap 2: Missing Null Terminator on Substrings:
std::string::c_str()is guaranteed to end in\0.std::string_viewis NOT null-terminated! Slicingsv.substr(0, 5)points to the first 5 characters inside a larger buffer with no\0after character 5.- Passing
sv.data()to C APIs (printf("%s"),fopen(),strlen()) causes the C function to read past the slice into adjacent memory until finding a random\0, triggering buffer over-reads and data leaks! - Fix: Convert to
std::string(sv)or copy into a null-terminated buffer before crossing C API boundaries.
std::string full = "admin:secret";
std::string_view sv = full;
std::string_view user = sv.substr(0, 5); // "admin"
// printf("%s\n", user.data()); // CRASH / BUG: prints "admin:secret"! No '\0' after 'n'!
// SAFE FIX: Use std::string or length-limited specifier:
std::println("{}", user); // OK: uses user.size()!
std::string nullTerminatedUser(user); // OK: creates null-terminated copy for C APIsNever pass sv.data() to functions expecting null-terminated C-strings; convert to std::string or copy into a null-terminated buffer.
C++20 std::span<T>: The Universal Contiguous Memory View
C++20 std::span<T>: The Universal Contiguous Memory View
While std::string_view is specialized for characters, C++20 std::span<T> creates a non-owning window over any contiguous sequence of typed elements:
- 16-byte Layout: Stores a pointer
T* data_(8 bytes) and element countsize_t size_(8 bytes). - Universal Parameter Target: Replaces cumbersome C-style pointer-plus-size pairs
(T* arr, size_t n)and works seamlessly with:std::vector<T>std::array<T, N>- Stack C-arrays
T arr[N]
- Read & Write Access: Unlike
std::string_view(which is strictly read-only),std::span<T>allows direct in-place mutation of caller elements. Usestd::span<const T>for read-only guarantees.
void zeroEven(std::span<int> data) { // works with vector, std::array, or raw array!
for (int& x : data) {
if (x % 2 == 0) x = 0; // mutates caller's memory in place
}
}
std::vector<int> v = {1, 2, 3, 4};
zeroEven(v); // zero-overhead span conversionUse std::span<T> instead of (T* arr, size_t n) or const vector<T>& to accept any contiguous buffer with zero allocation.
API Design Guidelines: string_view vs const string& vs string
API Design Guidelines: string_view vs const string& vs string
Choosing the right string parameter signature is a common staff-level interview prompt:
- Accept
std::string_viewby value:- Best for read-only inspection or substring tokenization.
- Accepts string literals (
"literal"),std::string, and char buffers with zero temporary heap allocations.
- Accept
const std::string&:- Use only when the function must pass the string directly into a legacy C API requiring a guaranteed null-terminated
const char*via.c_str().
- Use only when the function must pass the string directly into a legacy C API requiring a guaranteed null-terminated
- Accept
std::stringby value +std::move:- Use when the function intends to keep ownership (e.g. constructor initializing a member variable).
- Rvalues (
std::string(...)orstd::move(...)) move their heap buffer directly in O(1) with zero copy overhead.
// Optimal constructor pattern using value + move:
class User {
std::string name_;
public:
// 1 copy if lvalue passed, 0 copies (pure move) if rvalue passed:
explicit User(std::string name) : name_(std::move(name)) {}
};Accept string_view for read-only inspection, string by value + move when sinking ownership, and const string& only when null-termination is strictly required.
Unlock Live Implementation & Interview Scenarios
The C++23 production code, live coding steps, scenario twist solutions, and common UB traps are part of Brute2Best Pro. Module 1 is completely free.
First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.