C++ / STANDARD CONTAINERS
string_view and the dangling view trap
Use std::string_view as a borrowed pointer plus length over existing characters, and recognise the lifetime bugs where the view outlives its buffer.
What you will learn
- Read a string_view as a const char* plus a length with no ownership of the characters.
- Take read-only text parameters by string_view to accept literals, strings, and slices.
- Spot dangling views from temporaries, returned strings, and reallocating owners.
- Build std::string{sv} when you need to store the text or need a null terminator.
Understanding string_view and the dangling view trap
A std::string_view is two data members: a pointer to the first character and a length. It owns nothing, allocates nothing, and copies no characters when you build it from a std::string, a string literal, or another view, which is why substr on a view is pointer arithmetic while substr on a std::string allocates and copies. Because it carries a length instead of relying on a terminator, a view can name the middle of a buffer: it can start at index 4 and stop at index 9 without any character being touched or moved.
The size of a view tells you nothing about how long those characters live. Writing std::string_view id = std::to_string(n); compiles because std::string converts to a view implicitly, but the temporary string is destroyed at the semicolon and the view is left pointing at freed heap memory. Lifetime extension does not rescue you here: binding a temporary to const std::string& keeps it alive for the reference's scope, whereas a string_view is an ordinary object that merely copied a pointer, so there is nothing for the compiler to extend. The read that follows is undefined behaviour, and it often appears to work because the freed bytes have not been reused yet.
The other half of the trap is that a living owner can still move its characters. Anything that may reallocate a std::string - append, +=, push_back, insert, reserve - hands it a fresh buffer and turns every existing view into a dangling one, exactly as it invalidates iterators into a std::vector. A useful rule is direction of flow: views are safe travelling down into a function that only reads them, and risky travelling up or sideways, so reach for std::string_view in parameters and short-lived locals, and std::string for data members, container elements, and anything returned to a caller.
<iostream>
<string>
<string_view>
int main() {
std::string owner = "the quick brown fox";
std::string_view all{owner}; // no allocation, no copy
std::string_view word = all.substr(4, 5);
std::cout << "word: " << word << " (size " << word.size() << ")\n";
std::cout << "shares owner's buffer: "
<< (word.data() == owner.data() + 4) << '\n';
owner[4] = 'Q'; // same buffer, view still valid
std::cout << "after in-place edit: " << word << '\n';
const char* before = owner.data();
owner.append(200, '!'); // forces a new, larger buffer
std::cout << "buffer moved: " << (owner.data() != before) << '\n';
std::cout << "word is now dangling, so we never read it again\n";
}
A string_view borrows characters it does not own, so its validity is bounded by both the lifetime and the address stability of the buffer behind it.
Worked examples
Temporary owner, dangling view
Shows why a view initialised from a std::string returned by value dangles, and two ways to keep the characters alive.
<iostream>
<string>
<string_view>
std::string make_id(int n) { return "user-" + std::to_string(n); }
int main() {
// std::string_view bad = make_id(7); // temporary gone at the ';'
std::string kept = make_id(7); // named owner outlives the view
std::string_view good = kept;
std::cout << good << '\n';
// the temporary lives to the end of this full expression, so this is fine
std::cout << std::string_view{make_id(8)}.substr(5) << '\n';
}
Example explained
Line 1make_id returns by value, so its result is a temporary destroyed at the end of the statement that created it.
Line 2The commented-out line would compile without a warning on many builds, because std::string converts to std::string_view implicitly.
Line 3Storing the result in kept first gives the characters a named owner whose lifetime covers every use of good.
Line 4The last line is safe because the temporary still exists while operator<< runs; it becomes a bug the instant that view is saved in a variable.
data() is not a C string
Demonstrates that string_view::data() carries no terminator, so C functions read past the end of the view.
<cstring>
<iostream>
<string>
<string_view>
int main() {
std::string owner = "12:34";
std::string_view hh = std::string_view{owner}.substr(0, 2);
std::cout << hh << '\n';
std::cout << hh.size() << '\n';
std::cout << std::strlen(hh.data()) << '\n';
std::string copy{hh}; // explicit copy, now terminated
std::cout << std::strlen(copy.c_str()) << '\n';
}
Example explained
Line 1hh is a two-character window into owner, so hh.data() is simply owner.data() and hh.size() is 2.
Line 2strlen knows nothing about the view's length and keeps reading to owner's own terminator, reporting 5 instead of 2.
Line 3std::string copy{hh} is explicit on purpose: it copies exactly hh.size() characters and appends the terminator.
Line 4Only copy.c_str() is a valid argument for an API that expects a null-terminated string.
Zero-copy splitting
Splits one buffer into fields and prints each field's offset to show all views point back into the original string.
<iostream>
<string>
<string_view>
<vector>
std::vector<std::string_view> split(std::string_view s, char sep) {
std::vector<std::string_view> out;
for (;;) {
auto pos = s.find(sep);
out.push_back(s.substr(0, pos));
if (pos == std::string_view::npos) break;
s.remove_prefix(pos + 1);
}
return out;
}
int main() {
std::string csv = "red,green,blue";
const char* base = csv.c_str();
for (std::string_view f : split(csv, ',')) {
std::cout << f << ' ' << (f.data() - base) << '\n';
}
}
Example explained
Line 1remove_prefix advances the view's own start past the separator; the characters in csv are never copied or modified.
Line 2substr(0, npos) yields the remainder of the view, which is how the loop emits the final field before breaking.
Line 3The offsets 0, 4 and 10 prove the three fields are windows into csv rather than three small strings.
Line 4Calling split(std::string("a,b"), ',') and keeping the returned vector would leave every element pointing at a destroyed buffer.
Important notes
No lifetime extension applies to views: const std::string& r = f(); is safe, std::string_view v = f(); is not, even though both look like they hold on to the temporary.
remove_prefix and remove_suffix only move the view's own bounds, and a view gives read-only access, so use std::string or std::span<char> when you need to write characters.
Common mistakes
Writing std::string_view sv = std::to_string(n); or sv = a + b; - the temporary dies at the semicolon and every later read is freed memory, which frequently prints correctly in a debug build and garbage in an optimised one.
Passing sv.data() to printf("%s", ...) or strlen - there is no terminator at sv.size(), so the call keeps reading until it happens to meet a \0 and prints extra characters or overruns the buffer.
Keeping a string_view as a class member or inside a returned container built from a temporary - the object outlives the characters, so the field looks valid until something else reuses that memory.
Try it yourself
Change, predict, then run
Write std::string_view trim(std::string_view s) that strips leading and trailing spaces using remove_prefix and remove_suffix, then trim a named std::string and print the result. Then write down, without running it, why auto t = trim(std::string(" hi ")); is a bug even though it compiles.
Open the C++ workspaceCheck your understanding
Given std::string s, why does std::string_view sv = s + "!"; std::cout << sv; misbehave while const std::string& r = s + "!"; std::cout << r; is fine?
- operator+ returns a string whose data() is not null-terminated, so the view reads past the end of it.
- Lifetime extension works for string_view too, but only until the end of the enclosing block, so the view has already gone stale by the time it prints.
- The temporary string is destroyed at the end of the initialising full expression, and unlike a const reference a string_view has nothing to extend because it only copied a pointer and a length.
- std::cout << turns the view back into a temporary std::string, and that conversion reuses the buffer the concatenation already released.
Show answer
Concatenation produces a temporary std::string. Binding that temporary to a const reference extends its lifetime to the reference's scope; a string_view is not a reference, so the temporary dies at the semicolon and the view points at freed memory. Option 1 is tempting because printing inside the same statement really does work, but that is because the temporary survives to the end of the full expression, not because anything was extended for the view.