C++ String View Explained: When to Use It and Lifetime Rules
A C++17 string view is a non-owning pointer and length with an O(1) substr, the best read-only text parameter. When it dangles, and converting back to a string.
- Course: C++ study plan
- Module: Strings
- Kind: Lesson
- Reading time: 13 min
- Runtime: Clang 18 · C++20
What is std::string_view in C++?
std::string_view (C++17) is a non-owning, read-only view of characters that someone else owns: just a pointer and a length, sixteen bytes on x86-64 and cheap to copy. It supports the read-only string interface, including find and an O(1) substr, and accepts literals and std::string without copying. It must never outlive, or survive a modification of, the text it points at.
Lesson
Every std::string you pass, slice or compare costs a copy somewhere: a function taking const std::string& allocates when called with a literal, substr copies what it returns, and a parser that cuts a line into fields copies the whole line again in pieces. std::string_view (C++17) answers all three: a pointer and a length that refer to characters someone else owns. It is sixteen bytes on this platform, trivially copyable, its substr costs nothing — and it brings the one lifetime rule this lesson exists to make you respect.
What it is
#include <string_view>
std::string owner = "codekairo";
std::string_view whole = owner; // implicit from std::string: points at owner's buffer
std::string_view lit = "literal"; // from a literal: strlen once, no copy
std::string_view part(owner.data() + 4, 5); // pointer + length: "kairo"
std::cout << whole.size() << ' ' << part << ' ' << lit.substr(0, 3) << '\n'; // 9 kairo lit
A view holds data() and size() and nothing else — no buffer, no ownership, no null terminator of its own; copying one copies two words. It supports the whole read-only string interface: size, empty, [], at, front, back, iteration, the find family, substr, starts_with/ends_with, compare and all six comparison operators, which compare content just as std::string does. Anything that would change the characters — +=, push_back, insert, replace — does not exist, because the view does not own them. Two operations exist only on views: remove_prefix(n) and remove_suffix(n) shrink the window from either end in O(1), and are how a view is trimmed.
std::string_view v = " padded ";
v.remove_prefix(v.find_first_not_of(' ')); // "padded "
v.remove_suffix(v.size() - v.find_last_not_of(' ') - 1); // "padded"
substr in O(1)
std::string line = "2026-09-17T10:15:00";
std::string_view sv = line;
std::string_view date = sv.substr(0, 10); // "2026-09-17": a narrower window, no allocation
std::string_view time = sv.substr(11); // "10:15:00"
std::string::substr allocates and copies; std::string_view::substr adjusts a pointer and a length in constant time. A tokeniser written with views allocates nothing until you decide to keep something:
#include <vector>
std::vector<std::string_view> words(std::string_view text) {
std::vector<std::string_view> out;
std::size_t i = 0;
while (i < text.size()) {
while (i < text.size() && text[i] == ' ') ++i; // skip separators
const std::size_t start = i;
while (i < text.size() && text[i] != ' ') ++i; // run to the end of the word
if (start < i) out.push_back(text.substr(start, i - start));
}
return out;
}
Every element of out points into text's characters. That is the whole point, and the whole danger.
The right parameter type for read-only text
| Parameter | Called with a literal | Called with a std::string | Knows its length | Null-terminated |
|---|---|---|---|---|
const char* | fine | needs .c_str() | no — strlen | yes |
const std::string& | constructs a temporary std::string (allocates past 15 chars) | fine, no copy | yes | yes |
std::string_view | fine, no copy | fine, no copy | yes | not guaranteed |
Take std::string_view by value — two words; a reference would be an indirection for nothing — whenever the function only reads text: counting, searching, parsing, printing. The caller passes a literal, a std::string, another view or a (pointer, length) pair and nothing is copied. Return a std::string, not a view, when the function produces text.
int count_char(std::string_view text, char c) {
int n = 0;
for (char ch : text) if (ch == c) ++n;
return n;
}
count_char("banana", 'a'); // 3 - no std::string was ever built
count_char(owner, 'k'); // 1 - no copy of owner
The lifetime rule
A view is valid exactly as long as the characters it points at. Nothing in the language checks this, and the failures are undefined behaviour that often appears to work:
std::string_view bad1 = std::string("temporary"); // the string dies at the ';' - dangling immediately
std::string_view bad2 = owner + "!"; // same: + returns a temporary
std::string_view first_word() {
std::string line;
std::getline(std::cin, line);
return std::string_view(line).substr(0, line.find(' ')); // line is destroyed on return
}
std::vector<std::string> names = {"ada"};
std::string_view n0 = names[0];
names.push_back("grace"); // may reallocate: n0 now points at freed memory
The rule in one sentence: a view must not outlive the string it looks at, and that string must not be modified while the view is in use. In practice: take views as parameters and use them within the call; return a view only into something the caller gave you (the argument, never a local); never store a view in a struct or container unless the owner is guaranteed to live longer and stay put. words() above is fine because its caller still holds text; it becomes a bug the moment someone writes auto ws = words(read_line()); — the temporary line is gone by the next statement.
One more: data() is not guaranteed to end in '\0'. A view of the middle of a string stops where size() says but the bytes continue, so sv.data() handed to a C function that expects a terminator reads past the window. Build a std::string first.
Converting back
std::string_view sv = "kairo";
std::string s1(sv); // explicit construction: copies the characters
std::string s2 = sv; // error: the conversion from a view is explicit
std::string s3;
s3 = sv; // fine: assignment from a view is allowed
s3 += sv; // fine: append accepts a view
std::string s4 = std::string(sv) + "!"; // build the string, then concatenate
The asymmetry is deliberate: std::string converts to a view silently because that is free; a view becomes a std::string only when you write it, because that allocates. To keep a piece of text — a map key, a struct member, anything that outlives the input — convert and store the std::string.
constexpr views
constexpr std::string_view kVersion = "1.4.2";
constexpr std::string_view kMajor = kVersion.substr(0, kVersion.find('.')); // "1", at compile time
static_assert(kMajor == "1");
constexpr std::string_view kDays[] = {"mon", "tue", "wed", "thu", "fri", "sat", "sun"};
A view onto a literal is a constexpr value: the characters live in the program image for its whole run, so the lifetime rule holds automatically, and find, substr and == are constexpr. A std::string cannot be a constexpr variable in C++20 (its allocation must not escape compile-time evaluation), so a compile-time table of names is a table of views. Module 16, Modern C++, has more.
When not to use it
- The function stores the text (a member, a container element, a key): take
std::stringby value and move it in (Module 9, Copies, moves and the rule of five). - You need a null-terminated
const char*for a C API:std::stringandc_str(). - The text will be modified: views are read-only windows.
- The source is a temporary or will change while the view is alive: copy instead.
Pitfalls
std::string_view v = s + "x";or= std::string(...)dangles before the next statement.- Returning a view of a local, or of a
std::stringparameter taken by value, dangles. - A view into a
std::vector<std::string>element dies onpush_back; into astd::stringon any resize. sv.data()is not null-terminated; never hand it toprintf,fopenorstrlen.remove_prefix(n)withn > size()is undefined behaviour; computenfrom afindthat succeeded.
Key takeaways
std::string_viewis a pointer and a length: non-owning, sixteen bytes, trivially copyable, read-only.substr,remove_prefixandremove_suffixare O(1) and never allocate; thefindfamily and comparisons work as onstd::string.- Take it by value as the parameter type for any function that only reads text; return
std::stringwhen producing text. - A view must not outlive its string or survive a modification of it; never view a temporary, never return a view of a local.
- Converting back is explicit —
std::string(sv)— because it copies;data()is not null-terminated; views of literals areconstexpr.
Common questions
Should I use string_view or const std::string& for parameters?
Take std::string_view by value when a function only reads text: a literal, a std::string or another view all pass without a copy, whereas const std::string& builds a temporary string — allocating past 15 characters — when called with a literal. Take std::string when the function stores the text or needs a null-terminated c_str().
When does a string_view dangle?
When the characters it points at are destroyed or moved. std::string_view v = s + "x"; dangles at once, because + returns a temporary; returning a view of a local string dangles; and a view of a std::vector<std::string> element dies when push_back reallocates. A view must not outlive its string, and that string must not change while it is in use.
How do you convert a string_view to a std::string?
Construct one explicitly: std::string s(sv); or std::string(sv). std::string s = sv; does not compile, because the conversion copies and allocates and the library makes you ask for it; assigning s = sv; and appending s += sv; to an existing string are allowed.
Is string_view null-terminated?
Not necessarily. A view of part of a string stops where size() says, but the bytes after it continue, so sv.data() passed to printf, fopen or strlen reads past the window. Build a std::string and use its c_str() when a C API needs a terminator.
Why is string_view substr faster than string substr?
std::string::substr allocates a new string and copies the characters, while std::string_view::substr only adjusts a pointer and a length, in constant time. A tokeniser built from views allocates nothing until you convert a piece you want to keep into a std::string.
Exercises
Words as views
Read one line. Write std::vector<std::string_view> words(std::string_view text) that splits the text on runs of spaces without copying a single character — every element is a view into the line, which stays alive in main. Then print words=<n>, longest=<word> (the first of the longest words; longest=(none) if there are no words), and one line <word> <length> per word in order.
Input: one line (possibly empty, possibly with leading, trailing or repeated spaces). Output: two lines, then one line per word.
Example: input the quick brown fox → output
words=4
longest=quick
the 3
quick 5
brown 5
fox 3Prefix filter
The first line is a non-empty prefix. Every following line, until end of input, is an entry. Write std::string_view trim(std::string_view) that removes leading and trailing spaces and tabs with remove_prefix and remove_suffix, and for every entry: trim it, and if the trimmed entry starts_with the prefix print match: followed by the rest of the entry after the prefix. Finally print matched=<k> of <total>, where total counts every entry line, blank ones included.
Input: the prefix, then any number of entry lines. Output: one line per matching entry, then the summary.
Example: input
err
error: disk full
errno 5
warning: low
err
→ output
match: or: disk full
match: no 5
match:
matched=3 of 4