Skip to content
C++
C++17Intermediate

std::string_view — Zero-Copy String Handling

std::string_view (C++17) is a lightweight, non-owning reference to a contiguous sequence of characters. It stores just two things: a pointer to the start of the sequence and a length. No allocation, no copy, no ownership. It can point into a std::string, a string literal, a char[] array, or any other character storage, all without copying the data. This makes it the ideal type for function parameters that accept read-only string arguments.

The problem: unnecessary copies and conversions

Before string_view, the idiomatic way to accept a read-only string was const std::string&. This works well when the caller has a std::string, but it forces a heap allocation when the caller passes a string literal or a const char* — a temporary std::string is constructed and immediately destroyed.

// Pre-C++17 problem:
void log(const std::string& msg);

log("startup complete");         // constructs a temporary std::string from literal!
log(buffer.data());              // another temporary heap allocation

// C++17 solution:
void log(std::string_view msg);  // no allocation, no copy

log("startup complete");         // just a pointer + length into the literal
log(buffer.data());              // just a pointer + length, no allocation
log(my_string);                  // implicit conversion, still no copy
log(my_string.substr(5, 10));    // substr returns string_view (not a copy)

Constructing a string_view

string_view is implicitly constructible from almost any string-like source. It adapts to whatever underlying storage you have without touching the data.

#include <string_view>

// From a string literal:
std::string_view sv1 = "hello, world";    // pointer to static storage, len=12

// From a std::string (implicit):
std::string s = "example";
std::string_view sv2 = s;                 // points into s's buffer

// From a raw pointer + length:
const char buf[] = "raw data here";
std::string_view sv3(buf, 8);             // "raw data", len=8

// From another string_view:
std::string_view sv4 = sv1.substr(7, 5); // "world" — no copy, O(1)

// Size and content:
sv1.size();    // 12
sv1.length();  // 12 (same as size)
sv1.empty();   // false
sv1.data();    // const char* to the first character (NOT null-terminated in general!)

Key operations

string_view supports most read-only operations of std::string — subscript, find, comparison, iteration — all without any allocation. Notably, its substr() returns another string_view (not a new std::string), making substring operations O(1).

std::string_view sv = "hello, world!";

// Character access:
sv[0];                // 'h' — no bounds check
sv.at(0);             // 'h' — throws std::out_of_range if out of bounds
sv.front();           // 'h'
sv.back();            // '!'

// Substring — O(1), returns a view (no allocation):
sv.substr(7, 5);      // "world"

// Finding:
sv.find("world");     // 7
sv.find("xyz");       // std::string_view::npos

// Trimming helpers (C++20):
sv.starts_with("he"); // true
sv.ends_with("!");    // true

// Shrinking the view (modifies the view, not the data):
sv.remove_prefix(7);  // sv is now "world!" — original data untouched
sv.remove_suffix(1);  // sv is now "world"

// Comparison — compares contents, not addresses:
std::string_view a = "apple", b = "banana";
a < b;    // true (lexicographic)
a == "apple";  // true

The dangling view hazard

Because string_view is non-owning, it is only valid while the underlying data lives. Store a view longer than its source, or return a view that points into a local std::string, and you get undefined behavior. This is the most common source of bugs with string_view.

// BUG: returning a view into a local string:
std::string_view bad() {
    std::string local = "hello";
    return local;    // local is destroyed when the function returns!
}                    // the returned view is dangling — UB to use it

// BUG: view outlives the string:
std::string_view sv;
{
    std::string s = "temporary";
    sv = s;          // sv points into s's buffer
}                    // s is destroyed here — sv is now dangling

// BUG: std::string temporary:
std::string_view sv2 = std::string("hello");
// The temporary std::string is immediately destroyed after this line — sv2 dangles

// SAFE: view of a string literal (static storage — lives forever):
std::string_view sv3 = "literal";  // always valid

// SAFE: view used within the lifetime of the source:
void process(std::string_view sv);
std::string data = "persistent";
process(data);   // ok, data outlives the call

The rule: only store a string_view as a member variable or long-lived local if you can guarantee the source outlives it. For function parameters, string_view is almost always safe — the argument outlives the call.

string_view vs const std::string&

Use string_view when…

  • →Writing a function that reads (but does not store) a string argument — the canonical use
  • →Doing many substring operations — substr() is O(1) with no allocations
  • →Accepting any string-like input: literals, std::string, char arrays, all work without conversion

Keep const std::string& when…

  • →You need to pass the string to an API that takes const std::string& — to avoid a copy at the boundary
  • →You need a null-terminated C string (string_view::data() is NOT guaranteed to be null-terminated)
  • →You need to store the string beyond the current call — store a std::string, not a view

Key rules to remember

string_view is non-owning — it does not extend the lifetime of the source

Only use string_view for values you know will outlive the view. Function parameters are almost always safe; stored members require care.

string_view::data() is NOT guaranteed to be null-terminated

It's a pointer and a length. If you need a null-terminated string for a C API, convert to std::string first: std::string(sv).

substr() on string_view is O(1) and allocation-free — unlike std::string::substr()

std::string::substr() allocates a new std::string. string_view::substr() just adjusts the pointer and length.

Prefer string_view over const std::string& for read-only function parameters

It accepts any string-like argument without conversion, never allocates, and is typically cheaper to pass (two words vs one pointer with possible indirection).

Sign in to track progress