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"; // trueThe 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 callThe 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).