2Brute2Best
πŸ”₯0
M5.6

Strings, Small String Optimization (SSO), string_view & span

Medium High-frequency interview topic Pro OnlySmall String Optimization (SSO)std::string_view Zero-CopyDangling View Trapsstd::span<T>
⚑ 15-Second Mental ModelM5.6 · Instant Intuition
Single-Read Mastery
The Real-World Physical Analogy (Small String Optimization (SSO))

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.

Rule to Remember: Strings <= 15 chars never touch the heap allocator. Fast, cache-friendly.
3 Golden Facts to Remember
Fact #1
What is Small String Optimization (SSO), and how does std::string implement it without increasing sizeof(string)?

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.

Fact #2
What is the memory layout of std::string_view and std::span<T>, and why should they always be passed by value?

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.

Fact #3
Why is calling sv.data() from a substring view dangerous when passing to legacy C APIs?

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.

01

Interviewer's lens

What separates a strong C++ candidate from a textbook answer

String handling instantly reveals whether an engineer understands memory boundaries or treats C++ like a managed runtime:

  • Textbook trap: Assuming every std::string invokes new, passing const std::string& everywhere, and falling into dangling std::string_view lifetime 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++20 std::span<T> for contiguous buffer mutation.
02

Under the hood & memory layout

Hardware & memory representation

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, and capacity.
  • std::string_view is a Magnifying Glass (16 Bytes): Owns nothing. Stores just two 64-bit values: const char* ptr and size_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 of T (std::vector, std::array, or raw C buffers).
std::string SSO Stack Mode (≀ 15 chars)

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.

std::string Heap Mode (> 15 chars)

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[].

std::string_view Layout (16 bytes)

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.

std::span<T> Contiguous View (16 bytes)

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.

03

Core concepts & syntax traps

01

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:

  1. 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!
  2. Spill Mode (length >= 16): Allocates an external buffer on the heap. _M_dataplus points to that heap memory, and _M_allocated_capacity tracks 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!

C++23 snippet
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)
Interview takeaway:

Short strings (≀ 15 characters on GCC) live completely on the stack with zero heap allocation and instant cache locality.

02

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) and size_t len_ (8 bytes).
  • O(1) Substrings: sv.substr(offset, count) simply evaluates ptr_ + offset and adjusts length. Zero heap allocations, zero character copies.
  • Pass By Value: Always pass std::string_view by value, never by const&. On 64-bit ABIs, 16-byte structs pass directly inside two CPU registers (rsi, rdx), completely avoiding memory indirection.
C++23 snippet
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
}
Interview takeaway:

Pass std::string_view by value: 16 bytes in CPU registers beats pointer indirection and eliminates all heap allocations.

03

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_view is NOT null-terminated! Slicing sv.substr(0, 5) points to the first 5 characters inside a larger buffer with no \0 after 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.
C++23 snippet
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 APIs
Interview takeaway:

Never pass sv.data() to functions expecting null-terminated C-strings; convert to std::string or copy into a null-terminated buffer.

04

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 count size_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. Use std::span<const T> for read-only guarantees.
C++23 snippet
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 conversion
Interview takeaway:

Use std::span<T> instead of (T* arr, size_t n) or const vector<T>& to accept any contiguous buffer with zero allocation.

05

API Design Guidelines: string_view vs const string& vs string

Choosing the right string parameter signature is a common staff-level interview prompt:

  1. Accept std::string_view by value:
    • Best for read-only inspection or substring tokenization.
    • Accepts string literals ("literal"), std::string, and char buffers with zero temporary heap allocations.
  2. 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().
  3. Accept std::string by value + std::move:
    • Use when the function intends to keep ownership (e.g. constructor initializing a member variable).
    • Rvalues (std::string(...) or std::move(...)) move their heap buffer directly in O(1) with zero copy overhead.
C++23 snippet
// 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)) {}
};
Interview takeaway:

Accept string_view for read-only inspection, string by value + move when sinking ownership, and const string& only when null-termination is strictly required.

Brute2Best Pro Content

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.

All 440 visual dry runs & algorithms
Interactive step-by-step playback with pointer & array state transitions
52 comprehensive LLD chapters
Interactive UML diagrams, real-world stories (Food Delivery, Trading, RPG), and practice solutions
40 C++ basics labs & hardware memory models
Vtables, byte layouts, assembly insights, and multi-threading race conditions
Production-ready C++23 source code
Modern idioms, clean contracts, time/space complexity derivations, and interview takeaways

First anchor question per pattern, Part 1 LLD & Module 1 C++ basics are 100% Free forever.

Next In Your Mastery Journey

Apply C++ Internals to LLD & Algorithmic Patterns

Full-stack systems engineering