std::string_view
"Non-owning, read-only string reference (C++17): zero-copy substrings, universal parameter type, constexpr usage, and lifetime hazards."
std::string_viewsince C++17A non-owning, read-only view into a contiguous character sequence β stored as a (pointer, length) pair β that provides the full string inspection API without any allocation or copying.
Overview
Before C++17, functions accepting read-only strings faced an unavoidable trade-off:
const char*β no allocation, but loses length information and offers no member functions.const std::string&β full API, but forces a heap allocation when the caller passes a string literal or achar*.std::stringby value β always copies, always allocates.
std::string_view resolves this trilemma. It is a lightweight pair (typically 16 bytes on 64-bit platforms) of a pointer and a length. It provides almost the entire std::string read-only interface, accepts any contiguous character source implicitly, and imposes zero allocation overhead regardless of the call site.
The full class template is std::basic_string_view<CharT, Traits>. The standard provides these aliases:
| Alias | CharT | Since |
|---|---|---|
std::string_view | char | C++17 |
std::wstring_view | wchar_t | C++17 |
std::u16string_view | char16_t | C++17 |
std::u32string_view | char32_t | C++17 |
std::u8string_view | char8_t | C++20 |
Every constructor and member function (except copy()) is constexpr since C++17, making string_view the natural currency for compile-time string processing. It satisfies contiguous_range and integrates directly with all <algorithm> and <ranges> machinery.
Syntax
Construction
#include <string_view>
// From string literal β zero overhead, constexpr C++17
constexpr std::string_view sv1 = "hello world";
constexpr std::string_view sv2{"hello", 5}; // explicit length, no null-term needed
// Literal suffix: produces string_view directly C++17
using namespace std::string_view_literals;
constexpr auto sv3 = "hello"sv; // type is string_view, not const char*
// From std::string β implicit conversion via operator string_view()
std::string s = "example";
std::string_view sv4 = s; // borrows s's buffer; s must outlive sv4
// From arbitrary char buffer β safe for non-null-terminated data
const char* buf = get_network_packet();
std::size_t len = packet_length();
std::string_view sv5{buf, len}; // does NOT assume '\0' at buf[len]Core member functions
std::string_view sv = "Hello, World!";
// Capacity
sv.size(); // 13 constexpr C++17
sv.empty(); // false
// Access β unchecked and checked
sv[0]; // 'H' β no bounds check
sv.at(0); // 'H' β throws std::out_of_range
sv.front(); // 'H'
sv.back(); // '!'
sv.data(); // const char* β NOT null-terminated in general!
// Zero-copy subranges β O(1) pointer arithmetic
sv.substr(7, 5); // "World" β new view, no copy
sv.remove_prefix(7); // sv is now "World!" β modifies the view object, not the data
sv.remove_suffix(1); // sv is now "World"
// Search β identical signatures to std::string
sv.find("World"); // 7
sv.find_first_of("aeiou"); // 1 ('e')
sv.rfind('o'); // 8
// Prefix / suffix tests C++20
sv.starts_with("Hello"); // true
sv.ends_with("!"); // true
// Substring containment test C++23
sv.contains("World"); // true
// Comparison β lexicographic, constexpr
sv == "Hello, World!";
sv.compare("hello"); // < 0 (case-sensitive, 'H' < 'h')Examples
Universal read-only string parameter
#include <string_view>
#include <print> // C++23
void log(std::string_view message) { // C++17 β accepts everything, copies nothing
std::println("[LOG] {}", message); // std::format accepts string_view natively C++20
}
std::string owned{"from std::string"};
log(owned); // no copy β view into owned's buffer
log("string literal"); // no allocation β view into rodata
log("hello"sv); // string_view literal directly C++17
log({buf, buf_len}); // raw buffer with explicit lengthThis is strictly superior to const std::string& for read-only parameters: it accepts the same types, plus non-null-terminated buffers, at lower cost.
constexpr enum-to-string
string_view's full constexpr support makes it ideal for compile-time string mapping. Returning a view of a string literal is safe because literals have static storage duration.
enum class Status { Unknown, Created, Connected };
[[nodiscard]] constexpr std::string_view to_string(Status s) noexcept { // C++17
switch (s) {
case Status::Unknown: return "Unknown";
case Status::Created: return "Created";
case Status::Connected: return "Connected";
}
return "<invalid>";
}
static_assert(to_string(Status::Created) == "Created"); // compile-time verifiedZero-allocation tokenization
#include <string_view>
#include <vector>
// All returned views borrow from the original buffer β zero heap allocations
std::vector<std::string_view> split(std::string_view str, char delim) {
std::vector<std::string_view> tokens;
std::size_t start = 0, end;
while ((end = str.find(delim, start)) != std::string_view::npos) {
tokens.push_back(str.substr(start, end - start));
start = end + 1;
}
tokens.push_back(str.substr(start));
return tokens;
}
// The caller must keep 'csv' alive for as long as 'fields' is used:
std::string csv = "alpha,beta,gamma";
auto fields = split(csv, ','); // three views into csv β no copiesHeterogeneous map lookup
The C++14 transparent comparator std::less<> allows std::map and std::set to search with a string_view key without constructing a temporary std::string:
#include <map>
#include <string>
#include <string_view>
std::map<std::string, int, std::less<>> registry; // note: std::less<>, not std::less<std::string>
registry["alpha"] = 1;
registry["beta"] = 2;
std::string_view key = "alpha";
auto it = registry.find(key); // O(log n), zero allocation C++14 transparent compareWithout std::less<>, even find(string_view) would silently construct a temporary std::string.
Best Practices
Use string_view for all read-only string parameters. It is a strict improvement over const std::string& in almost every case. The only exception: if you immediately need a std::string inside the function (and you cannot avoid the copy), taking std::string by value is better so the compiler can elide the copy at the call site.
Use the sv literal suffix for constants. "hello"sv is resolved at compile time to a string_view with no runtime construction; it also documents the intent to readers.
Return string_view only from stable storage. Safe sources: string literals (static duration), data members of a longer-lived object, or caller-provided buffers. Functions that build or transform strings must return std::string.
Leverage the constexpr interface. String searches, comparisons, and substrings are all constexpr since C++17. Use string_view in if constexpr branches and static_assert expressions where you previously had to rely on const char* tricks.
Remember std::hash is specialized. std::unordered_map<std::string, V> does not support heterogeneous lookup by default. If you need hash-based lookup with string_view keys, use a custom hasher or wait for C++26 std::unordered_map heterogeneous support.
Common Pitfalls
Dangling view from a temporary
// WRONG β concatenation produces a temporary std::string that dies immediately
std::string_view sv = first + " " + last; // UB: sv points to freed memory
// WRONG β returned local
std::string_view bad_return() {
std::string s = build_string();
return s; // UB: s is destroyed at the closing brace
}
// CORRECT β return std::string; NRVO eliminates the copy
std::string good_return() {
return build_string();
}Views into reallocated or moved strings
std::string source = "alpha,beta,gamma";
auto tokens = split(source, ','); // tokens hold views into source's buffer
source.reserve(1024); // may reallocate β all tokens now dangle!
source = "different"; // definitely reallocates β all tokens now dangle!Treat the backing string as immutable and alive for the entire lifetime of any derived views.
Passing to null-terminated C APIs
string_view::data() is not guaranteed to be null-terminated, especially after substr() or remove_prefix():
std::string_view sv = "hello world";
std::string_view partial = sv.substr(0, 5); // "hello" β no '\0' at position 5
// WRONG β UB if the next byte in memory is not '\0'
printf("%s", partial.data());
// CORRECT β explicit conversion or length-aware write
printf("%s", std::string(partial).c_str());
fwrite(partial.data(), 1, partial.size(), stdout);
std::println("{}", partial); // std::format / print handle this correctly C++23No direct concatenation
string_view intentionally omits operator+ to prevent accidental allocations. Concatenation requires an explicit destination:
std::string_view a = "hello", b = " world";
// auto s = a + b; // compile error
auto s = std::string{a} + std::string{b}; // two views β one owned string
auto s2 = std::format("{}{}", a, b); // via format C++20Substring operations without allocation
Every substr() call on a string_view is O(1) and allocation-free. It returns a new string_view that points into the same underlying buffer with adjusted pointer and length β no characters are ever copied. This makes repeated substring slicing practical in hot paths where std::string::substr would be too expensive.
// C++17
#include <string_view>
std::string_view parse_value(std::string_view line) {
// e.g. line = "Content-Type: text/html"
auto colon = line.find(':');
if (colon == std::string_view::npos) return {};
std::string_view value = line.substr(colon + 1); // O(1) β no copy
// strip leading spaces
while (!value.empty() && value.front() == ' ')
value.remove_prefix(1); // O(1) β adjusts pointer
return value; // "text/html" β still points into the original buffer
}
constexpr std::string_view path = "/usr/local/bin/myapp";
constexpr auto filename = path.substr(path.rfind('/') + 1); // "myapp" β constexpr C++17
static_assert(filename == "myapp");remove_prefix(n) and remove_suffix(n) are the in-place equivalents: they shrink the view without touching the data. Combined, they replace most "trim" implementations that previously needed temporary strings.
string_view in function parameters
string_view is the correct type for any read-only string parameter that does not need to be stored beyond the function call. It accepts const char*, std::string, std::string&, std::string&&, std::string_view, and raw {ptr, len} pairs β all with a single overload and zero allocation.
// C++17
#include <string_view>
#include <string>
// PREFER: one overload, accepts everything
std::size_t count_vowels(std::string_view text) {
std::size_t n = 0;
for (char c : text)
if (std::string_view{"aeiouAEIOU"}.find(c) != std::string_view::npos)
++n;
return n;
}
// All valid β no extra allocation in any case:
count_vowels("hello world");
count_vowels(std::string{"hello world"});
count_vowels(std::string_view{"hello", 5});
// AVOID for parameters that must outlive the call:
// void store(std::string_view sv) { stored_ = sv; } // risky β caller may not keep source alive
// Better: void store(std::string_view sv) { stored_ = std::string{sv}; }Pass by value β not const std::string_view&. A string_view is already 16 bytes, and taking a reference to it adds an indirection without any benefit.
Lifetime hazards and dangling views
A string_view does not own its data; it will silently dangle if the source is destroyed or mutated. The compiler cannot detect most of these bugs β they produce undefined behaviour at runtime.
// C++17 β common lifetime traps
// Trap 1: temporary std::string
std::string_view sv1 = std::string{"hello"}; // DANGLING β temporary destroyed at semicolon
// Trap 2: function returning a view of a local
std::string_view bad() {
std::string s = "built locally";
return s; // UB: s destroyed on return
}
// Trap 3: container reallocation invalidates all views into it
std::string buffer = "alpha beta gamma";
std::string_view word = buffer.substr(0, 5); // NOTE: string::substr returns std::string!
// correct version:
std::string_view word2{buffer.data(), 5}; // view into buffer
buffer += " delta"; // may reallocate β word2 may now dangle!
// Safe pattern: treat the owner as immutable for the view's lifetime
const std::string immutable = "alpha beta gamma";
std::string_view safe{immutable.data(), 5}; // safe as long as immutable is not modifiedThe safest rule: only store a string_view as a local variable, never as a data member, unless you can guarantee the source outlives the entire enclosing object.
Comparison and sorting
All six comparison operators are constexpr and defined for string_view, making it a drop-in for lexicographic ordering. Mixed comparisons between string_view and std::string (or const char*) are supported through implicit conversion.
// C++17
#include <string_view>
#include <algorithm>
#include <vector>
constexpr std::string_view a = "apple";
constexpr std::string_view b = "banana";
static_assert(a < b); // lexicographic, compile-time
// Sorting a vector of string_views β no allocations, no comparator needed
std::vector<std::string_view> words = {"cherry", "apple", "banana"};
std::ranges::sort(words); // uses operator< C++20
// Three-way comparison C++20
auto order = a <=> b; // std::strong_ordering::less
// compare() returns int β identical to std::string::compare
int r = a.compare(b); // r < 0
// Case-insensitive sort via explicit comparator
std::ranges::sort(words, [](std::string_view x, std::string_view y) {
return std::lexicographical_compare(
x.begin(), x.end(), y.begin(), y.end(),
[](char cx, char cy) { return std::tolower(cx) < std::tolower(cy); });
});Because string_view comparisons never allocate, sorting large collections of string-views into an index is significantly cheaper than sorting collections of std::string.
string_view vs span<char>
string_view and std::span<const char> are both non-owning views over character data, but they serve different abstractions. string_view is a string: it carries string-specific operations (find, substr, starts_with, comparisons against string literals). span<const char> is a range of bytes: it is appropriate when the data is binary, not textual.
// C++20
#include <string_view>
#include <span>
#include <cstddef>
// string_view β for text
void parse_header(std::string_view line) {
if (line.starts_with("Content-Type:")) // string API β natural
handle_content_type(line.substr(14));
}
// span<const char> β for raw bytes / binary protocols
void process_packet(std::span<const std::byte> payload) {
// byte-level access, no string semantics
}
// Interop: you can construct a span from a string_view's data
std::string_view sv = "raw data";
std::span<const char> as_bytes{sv.data(), sv.size()}; // legal but unusual
// Key differences:
// - string_view: find, substr, starts_with, ends_with, contains, compare
// - span<char>: subspan, first, last β no string search
// - string_view: null-terminated source assumed safe for display
// - span<byte>: no such assumption β generic contiguous memoryPrefer string_view whenever the data is human-readable text. Use span<const std::byte> or span<const char> for binary buffers, serialised data, or cryptographic payloads.
C++23 additions: contains, starts_with, ends_with
starts_with and ends_with shipped in C++20; contains arrived in C++23. All three are constexpr, accept a string_view, a single char, or a const char*, and replace common find-based idioms with clearer names.
// C++20 / C++23
#include <string_view>
constexpr std::string_view path = "/usr/local/include/vector";
// C++20
static_assert(path.starts_with("/usr")); // prefix check
static_assert(path.ends_with("vector")); // suffix check
static_assert(!path.starts_with('/')); // char overload
// C++23
static_assert(path.contains("local")); // substring check
static_assert(path.contains('/')); // char overload
static_assert(!path.contains("windows"));
// Replacing old find-based patterns:
// BEFORE (C++17): if (sv.find(sub) != std::string_view::npos) ...
// AFTER (C++23): if (sv.contains(sub)) ...
// All work at compile time:
constexpr bool has_include = path.contains("include"); // C++23
static_assert(has_include);
// Polyfill for pre-C++23: implement contains() as a free function
constexpr bool sv_contains(std::string_view haystack, std::string_view needle) noexcept {
return haystack.find(needle) != std::string_view::npos; // C++17
}If your compiler only targets C++17 or C++20, starts_with and ends_with are available from C++20 β check your toolchain's __cpp_lib_starts_ends_with feature-test macro before using them.
Hashing string_view
std::hash<std::string_view> is specialised in the standard library, so string_view works out of the box as a key in std::unordered_map and std::unordered_set. The hash value is guaranteed to equal std::hash<std::string> for the same character sequence, enabling heterogeneous lookups with a custom hasher.
// C++17
#include <string_view>
#include <unordered_map>
#include <string>
// Direct use β string_view as map key (you own the storage elsewhere)
std::unordered_map<std::string_view, int> view_map;
view_map["alpha"] = 1; // key is a view of the string literal; literal has static duration
// Hash consistency between string and string_view:
std::string s = "hello";
std::string_view sv = s;
assert(std::hash<std::string>{}(s) == std::hash<std::string_view>{}(sv)); // guaranteed
// Heterogeneous lookup in unordered containers requires a transparent hasher (C++20)
struct StringHash {
using is_transparent = void; // enables heterogeneous lookup
std::size_t operator()(std::string_view sv) const noexcept {
return std::hash<std::string_view>{}(sv);
}
};
struct StringEqual {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept { return a == b; }
};
// C++20 β unordered_map with heterogeneous lookup
std::unordered_map<std::string, int, StringHash, StringEqual> registry;
registry["alpha"] = 42;
std::string_view key = "alpha";
auto it = registry.find(key); // no temporary std::string constructed C++20Note: storing string_view as a map key is only safe when all viewed strings outlive the map. For general-purpose caches, use std::string keys and the transparent hasher above to avoid allocation on lookup.
See Also
std::stringβ owning counterpart; construct fromstring_viewwithstd::string{sv}std::spanβ analogous non-owning view for arbitrary element types (C++20)std::formatβ acceptsstring_viewnatively as a format argument (C++20)- Ranges library β
string_viewmodelscontiguous_range; works directly with all range algorithms