std::string_view
Non-owning view of a character sequence. Zero-copy alternative to const std::string&. Accepts literals, std::string, and char arrays without allocation.
std::string_viewsince C++17A non-owning view into a contiguous sequence of characters, providing efficient read-only access to strings without copying or allocation.
Overview
std::string_view solves a fundamental problem in pre-C++17 code: accepting string parameters efficiently. Before C++17, you had to choose between const std::string& (which forces allocation when passed string literals) and const char* (which requires overloads and lacks bounds information).
std::string_view is a tiny object—just a pointer and a length—that refers to a character sequence you don't own. It accepts string literals, std::string, char*, and substrings with zero allocation and zero copies. It's ideal for function parameters accepting read-only strings.
The critical constraint: never store it. The underlying data must outlive the view, or you have a dangling pointer.
Syntax
Construction
#include <string_view>
std::string_view sv1; // C++17: empty view
std::string_view sv2 = "hello"; // C++17: from literal
std::string_view sv3(ptr, len); // C++17: pointer + length
std::string_view sv4 = some_string; // C++17: from std::string (implicit)
// C++20: alternate notation
using namespace std::string_view_literals;
auto sv5 = "hello"sv; // C++17 as literal operatorCore interface
// C++17
sv.size(); // length in characters
sv.empty(); // true if size() == 0
sv.data(); // raw pointer (not guaranteed null-terminated)
sv[i]; // unchecked access
sv.at(i); // checked access (throws std::out_of_range)
sv.substr(pos, len); // returns new string_view (no copy)
sv.remove_prefix(n); // advance start by n
sv.remove_suffix(n); // shrink end by n
sv.find(x); // find char, string, or view; returns npos if not found
sv.compare(other); // lexicographic comparison
// C++20
sv.starts_with(x); // prefix check (x: char, string_view, or char*)
sv.ends_with(x); // suffix check
sv.substr(pos); // substring from pos to end (length deduced)
// C++23
sv.contains(x); // substring presence checkExamples
Replace overloads with a single parameter
// C++17 – before string_view
void process(const std::string& s); // needs std::string
void process(const char* s); // needs char*
// Callers of process("hello") force a temporary std::string
// C++17+ – with string_view
void process(std::string_view sv) {
std::cout << sv << '\n';
}
int main() {
process("hello"); // string literal – no alloc
std::string s = "world";
process(s); // std::string – no copy
process(std::string_view(s).substr(1, 3)); // substring – no copy
}Substring views without allocation
std::string_view text = "Hello, World!";
// All of these return a new string_view; none copy data
auto hello = text.substr(0, 5); // "Hello"
auto world = text.substr(7, 5); // "World"
auto end = text.substr(8); // "World!" (C++20)
std::cout << hello << " " << world << '\n'; // OK: views still validSplitting lines
#include <string_view>
#include <vector>
std::vector<std::string_view> split_lines(std::string_view text) {
std::vector<std::string_view> lines;
while (!text.empty()) {
auto pos = text.find('\n');
if (pos == std::string_view::npos) {
lines.push_back(text);
break;
}
lines.push_back(text.substr(0, pos));
text.remove_prefix(pos + 1);
}
return lines; // All views point into the original text
}
// Usage
std::string file_content = ...;
auto lines = split_lines(file_content); // zero copies
for (auto line : lines) {
std::cout << line << '\n';
}Constexpr string processing (C++17+)
constexpr std::string_view greeting = "Hello, World!";
constexpr auto len = greeting.size(); // 13 at compile time
// C++20: predicate functions
static_assert(greeting.starts_with("Hello"));
static_assert(greeting.ends_with("World!"));
// C++23: substring presence
static_assert(greeting.contains("lo, W"));As an enum-to-string converter (C++20+)
enum class Status { Unknown, Created, Connected };
constexpr std::string_view to_string(Status s) {
switch (s) {
case Status::Unknown: return "Unknown";
case Status::Created: return "Created";
case Status::Connected: return "Connected";
}
}
static_assert(to_string(Status::Created) == "Created");Best Practices
Use std::string_view for read-only string parameters. It replaces const std::string& and const char* overloads in almost all cases. A single void foo(std::string_view) parameter accepts all string types with zero allocation.
Pass substrings and ranges efficiently. If you need to process a portion of a string—splitting, trimming, tokenizing—use string_view views and remove_prefix()/remove_suffix(). All operations are O(1).
Store the underlying string, not the view. If you need to retain a string as a member variable, store std::string or keep a reference to the owning object. Never store string_view as a member unless you control the lifetime of the underlying data (e.g., a static buffer or a reference to a parent).
Convert to std::string only when you need ownership. If a function returns string_view and you need to keep the result beyond the caller's lifetime, construct std::string(view) at the call site.
For C APIs, convert explicitly. Don't pass sv.data() directly to C functions expecting const char* null-terminated strings. They're incompatible: the view may not be null-terminated. Convert with std::string(sv).c_str() or use temporary buffers carefully.
Common Pitfalls
Dangling views
// DANGER: view dangling after function return
std::string_view get_name() {
std::string local = "Alice";
return local; // UB – local destroyed, view is invalid
}
// DANGER: rvalue destroyed immediately
std::string_view sv = std::string("temporary");
// sv now points to freed memory
// DANGER: string reallocated, view invalidated
std::string s = "hello";
std::string_view sv = s;
s.append(" world"); // may reallocate; sv is now danglingLifetime must be guaranteed: the view cannot outlive the underlying data.
Data is not null-terminated
std::string_view sv = "hello world";
auto piece = sv.substr(0, 5); // "hello"
// WRONG:
std::printf("%s\n", piece.data()); // UB – piece is not null-terminated
// RIGHT: either construct a string or use the stream operator
std::cout << piece << '\n'; // operator<< handles string_view
auto s = std::string(piece);
std::printf("%s\n", s.c_str());Storing string_view as a class member is almost always wrong
// DANGEROUS
class Parser {
std::string_view input; // whose string is this? For how long?
public:
Parser(std::string_view src) : input(src) {}
};
std::string temp = "code";
Parser p(temp); // p.input will dangle when temp is destroyed
// SAFE – store or reference the owner
class Parser {
std::string input; // we own it
public:
Parser(std::string_view src) : input(src) {} // view is copied into owned string
};Modification not supported
std::string_view is read-only. If you need to modify the string, use std::string& instead.
Tokenizing with remove_prefix Chaining
The most allocation-free tokenization pattern: advance a string_view in-place using find and remove_prefix. No std::vector<std::string> needed — every returned token is a view into the original buffer.
// C++17
#include <string_view>
#include <optional>
// Returns the next token up to `delim` and advances `text` past it.
// When no delimiter is found, returns the remaining text (last token).
std::string_view next_token(std::string_view& text, char delim) {
auto pos = text.find(delim);
if (pos == std::string_view::npos) {
auto token = text;
text.remove_prefix(text.size()); // exhaust the view
return token;
}
auto token = text.substr(0, pos);
text.remove_prefix(pos + 1); // skip token + delimiter
return token;
}npos signals "no more delimiters" — the remaining view is the final token, so callers don't need a separate end-of-input check.
// C++17 – parse "key1=val1;key2=val2" with zero allocations
void parse_pairs(std::string_view input) {
while (!input.empty()) {
auto pair = next_token(input, ';'); // one "key=val" slice
auto key = next_token(pair, '='); // key portion
auto value = pair; // whatever is left
// All three views point into the original input buffer.
std::cout << "key=" << key << " value=" << value << '\n';
}
}
// Call site — one allocation: the original std::string.
std::string data = "key1=val1;key2=val2;key3=val3";
parse_pairs(data);Zero allocation for parsing. All returned views point into the original buffer. Only one allocation ever occurs — the one that created the source string — regardless of how many tokens are extracted.
std::string_view with std::ranges::views::split (C++20)
std::views::split produces a lazy range over subranges of a string_view, deferring work until each element is consumed. This composes naturally with other range adaptors.
// C++20
#include <string_view>
#include <ranges>
#include <iostream>
int main() {
std::string_view line = "a,b,c,d";
// views::split returns a range of subrange objects, not string_views.
// Wrap each subrange to get a proper string_view.
for (auto field_range : std::views::split(line, ',')) {
std::string_view field(field_range.begin(), field_range.end());
std::cout << field << '\n';
}
}In C++20, views::split elements are std::ranges::subrange<iterator>, not std::string_view directly. The explicit construction std::string_view(r.begin(), r.end()) is necessary.
// C++20 – combine with views::transform to collect all fields
#include <string_view>
#include <ranges>
#include <vector>
auto split_csv(std::string_view line) {
return std::views::split(line, ',')
| std::views::transform([](auto r) {
return std::string_view(r.begin(), r.end());
});
}
// C++23 – views::split with string_view is directly usable; syntax simplifies
// because string_view satisfies std::ranges::contiguous_range properly.
auto split_csv_23(std::string_view line) {
return std::views::split(line, ',')
| std::views::transform([](std::string_view sv) { return sv; });
// C++23: the element type is already string_view for contiguous_range inputs
}Use views::split when composing pipelines. For simple one-pass tokenization, the manual remove_prefix loop is lower overhead; views::split shines when chaining multiple range adaptors.
Comparison and Sorting Behavior
string_view::compare and all comparison operators follow lexicographic byte order, identical to std::string. All six relational operators (==, !=, <, <=, >, >=) are defined, making string_view totally ordered.
// C++17
#include <string_view>
#include <algorithm>
#include <vector>
std::string_view a = "apple";
std::string_view b = "banana";
bool less_than = (a < b); // true – 'a' < 'b'
int cmp = a.compare(b); // negative
bool equal = (a == "apple"); // true
// Sorting a vector of views — no copies of the underlying strings
std::vector<std::string_view> words = {"zebra", "ant", "mango", "fig"};
std::sort(words.begin(), words.end()); // uses operator< — lexicographic order
// words: ["ant", "fig", "mango", "zebra"]Case-insensitive comparison requires a comparator because string_view has no locale awareness:
// C++20 – case-insensitive sort via custom comparator
#include <string_view>
#include <algorithm>
#include <cctype>
auto iless = [](std::string_view lhs, std::string_view rhs) {
return std::ranges::lexicographic_compare(
lhs, rhs,
[](char l, char r) {
return std::tolower((unsigned char)l)
< std::tolower((unsigned char)r);
});
};
std::sort(words.begin(), words.end(), iless);All underlying strings must remain alive while views are in the sorted container. Sorting string_views rearranges pointers, not characters — the source buffers are untouched and must outlive the sorted range.
string_view in constexpr Contexts
std::string_view construction and most read operations are constexpr from C++17. find, substr, and related algorithms become constexpr in C++20/23, enabling rich compile-time string processing.
// C++17 – basic constexpr operations
constexpr std::string_view kPrefix = "Error: ";
static_assert(kPrefix.size() == 7);
static_assert(kPrefix[0] == 'E');
// C++17 – loop over characters at compile time
constexpr std::size_t count_occurrences(std::string_view text, char ch) noexcept {
std::size_t n = 0;
for (char c : text) n += (c == ch);
return n;
}
static_assert(count_occurrences("hello world", 'l') == 3);consteval functions that operate on string_view let you validate inputs or compute hashes at compile time with no runtime cost:
// C++20 – consteval: compile-time format-string validator
consteval bool is_valid_format(std::string_view fmt) {
// Every '{' must be followed by '}'
for (std::size_t i = 0; i < fmt.size(); ++i) {
if (fmt[i] == '{') {
if (i + 1 >= fmt.size() || fmt[i + 1] != '}') return false;
}
}
return true;
}
static_assert(is_valid_format("Hello {}!"));
// static_assert(is_valid_format("Hello {!")); // compile error
// C++17 – fixed-size char array round-trip
constexpr std::array<char, 5> buf = {'h','e','l','l','o'};
constexpr std::string_view from_array(buf.data(), buf.size());
static_assert(from_array == "hello");C++26 completes constexpr support. All major implementations are expected to fully support find and rfind in constexpr contexts by C++26; C++23 already covers most practical cases.
Wide and Unicode String Views
The <string_view> header provides four additional specialisations alongside std::string_view (char):
| Type | Character type | Typical use |
|---|---|---|
std::wstring_view | wchar_t | Windows WCHAR* APIs, wide literals |
std::u8string_view | char8_t | UTF-8 with type-level intent (C++20) |
std::u16string_view | char16_t | UTF-16 strings / u"" literals (C++17) |
std::u32string_view | char32_t | UTF-32 strings / U"" literals (C++17) |
// C++17 – u16 and u32 views from literals
std::u16string_view utf16 = u"Hello"; // char16_t sequence
std::u32string_view utf32 = U"Hello"; // char32_t sequence
// C++20 – u8string_view enforces UTF-8 semantic intent at the type level
std::u8string_view utf8 = u8"Hello"; // char8_t sequence
// C++17 – wstring_view for Windows APIs
std::wstring_view wide = L"Hello";
// Windows interop: wstring_view::data() is NOT null-terminated after substr
// — convert to std::wstring before passing to WINAPI functions.
std::wstring owned(wide);
::SetWindowTextW(hwnd, owned.c_str()); // safe: null-terminatedstring_view has no encoding knowledge. Every operation — find, substr, size — works on individual code units (bytes for char), not Unicode code points. u8string_view is API-identical to string_view; the char8_t type merely signals UTF-8 intent at compile time and prevents accidental mixing with char strings.
Accepting string_view in Generic Code
When writing templates or library interfaces, std::string_view is the preferred non-owning parameter type. It avoids the overload proliferation of const char* / const std::string& and is the only single-type parameter that accepts all string-like arguments without allocation.
// C++17 – string_view beats const string& for read-only parameters
void log_v1(const std::string& msg); // forces allocation for string literals
void log_v1(const char* msg); // requires second overload
void log_v2(std::string_view msg); // accepts all of the above — zero allocconst std::string& does not accept std::string_view — the implicit conversion goes the other way (string_view from string, not string from string_view). A string_view parameter is therefore the only way to accept both string and string_view without overloads.
// C++20 – concept-constrained generic function
#include <concepts>
#include <string_view>
template <std::convertible_to<std::string_view> S>
void log(S&& msg) {
log_impl(std::string_view(std::forward<S>(msg))); // one view, no allocation
}
log("literal"); // const char* → string_view, zero alloc
log(std::string("owned")); // string → string_view, zero alloc
log(std::string_view("view")); // already a view, trivialPass string_view by value, not by const reference. A string_view is 16 bytes (pointer + size) on all major platforms — smaller than a pointer-to-string indirection. void foo(std::string_view sv) is preferred over void foo(const std::string_view& sv).
// C++17 – correct: by value
void process(std::string_view sv); // preferred
// C++17 – redundant indirection: by const ref
void process(const std::string_view& sv); // adds an unnecessary pointer dereferenceSee Also
- std::string — owning string class
- std::span — equivalent for any contiguous container (C++20)
- std::format — formatted output with string_view support (C++20)