string_view and Non-Owning Views
Reviewed & published by Brayan K
A std::string_view is a tiny window — just a pointer and a length — over characters that live somewhere else. It lets you slice strings and accept text parameters with zero copying. The catch is that it doesn't own its data, so it can dangle. By the end you'll use views to make string code faster, and know exactly when they are safe.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- What a string_view is: a non-owning pointer + length
- Take zero-copy substrings with substr / remove_prefix
- Accept text parameters as string_view instead of const std::string&
- Recognise and avoid the dangling-reference danger
- Remember a view is not guaranteed null-terminated
- Generalise the idea to arrays with std::span<T>
💡 Real-World Analogy
A std::string is like photocopying a page of a book so you can keep your own copy. A std::string_view is like sliding a cardboard frame over part of the original page — you can read the words inside the frame, and you can move or shrink the frame instantly, but you never copy anything. The danger is obvious: if someone closes the book (destroys the string) or reshuffles the pages (reallocates the buffer) while your frame is still resting on it, the frame now sits over nothing. The frame only works while the page underneath stays put.
1. Owning vs Viewing
A std::string owns its characters: constructing one allocates memory and copies the data in. A std::string_view owns nothing — it stores only a pointer to the first character and a length, describing a window into characters that already exist. That makes copying a view trivially cheap (so you pass it by value), and it makes a substring of a view free: substr, remove_prefix, and remove_suffix just move the window's edges without touching the data.
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
int main() {
// A std::string OWNS its characters (it allocates and copies).
string owner = "Hello, world";
// A std::string_view is just a POINTER + LENGTH into existing data.
// No allocation, no copy -- it borrows the characters.
string_view view = owner;
cout << "size: " << view.size() << endl; // size: 12
// A substring of a VIEW is another view: it only shifts the pointer
// and shrinks the length. Zero characters are copied.
string_view hello = view.substr(0, 5); // "Hello"
cout << "slice: " << hello << endl; // slice: Hello
// remove_prefix / remove_suffix just move the window's edges:
string_view tail = view;
tail.remove_prefix(7); // drop "Hello, "
cout << "tail: " << tail << endl; // tail: world
return 0;
}
// ✅ Expected output:
// size: 12
// slice: Hello
// tail: world2. string_view as a Function Parameter
The classic read-only string parameter was const std::string&. But when a caller passes a string literal or a char array, that forces the construction of a temporary std::string — an allocation and a copy just to call the function. A std::string_view parameter binds directly to std::string, literals, and char arrays alike, with no allocation, while still offering size(), substr() and the other read operations. For read-only text, string_view is the modern default.
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// Accepting string_view means this function binds to ALL of:
// - a std::string (no copy)
// - a string literal (no temporary std::string built)
// - a char array / char*
// ...with zero allocations, while still offering size(), substr(), etc.
void printLine(string_view text) {
cout << "[" << text.size() << "] " << text << endl;
}
int main() {
string s = "from a std::string";
printLine(s); // [18] from a std::string
printLine("a string literal"); // [16] a string literal (no temp!)
// Compare: a 'const std::string&' parameter would force the literal
// above to be turned into a temporary std::string -> an allocation.
return 0;
}
// ✅ Expected output:
// [18] from a std::string
// [16] a string literalYour turn. Complete firstWord so it returns a view of the text before the first space — no copying allowed:
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// firstWord returns a VIEW of the first word -- no copying.
string_view firstWord(string_view s) {
auto space = s.find(' ');
// 🎯 YOUR TURN — return the part of s BEFORE the first space.
// Use substr from index 0 up to 'space' characters:
return ___; // 👉 s.substr(0, space)
}
int main() {
string sentence = "view borrows characters";
cout << firstWord(sentence) << endl; // view
// ✅ Expected output: view
return 0;
}🧩 Reorder Challenge
These lines should make a string, view it, slice the view, and print the slice. Put them in the right order:
A. string_view v = owner;
B. string owner = "Hello, world";
C. cout << v.substr(0, 5) << endl;
D. int main() {
E. return 0;
F. }Correct order: D → B → A → C → E → F
Open main (D), create the owning string (B), make a view of it (A), print a zero-copy slice (C), then return 0; (E) and close the brace (F). The owner must exist before the view.
3. The Dangling Danger
Because a view doesn't own its data, it is only valid while that data lives. Three patterns are undefined behaviour: returning a view of a local string (the local dies on return), storing a view of a temporary (the temporary dies at the end of the statement), and mutating the owner while a view of it exists (appending can reallocate the buffer, invalidating the view's pointer). The rule of thumb: a string_view must never outlive the data it points at, and the owner must not be reallocated underneath it.
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// ❌ BUG: returns a view of a LOCAL string. When makeView returns, the
// local 'temp' is destroyed -- the returned view dangles immediately.
string_view makeView() {
string temp = "I will be destroyed";
return temp; // 🚨 dangling: temp dies here
}
int main() {
// Using a dangling view is UNDEFINED BEHAVIOUR (garbage or a crash):
// string_view bad = makeView();
// cout << bad << endl; // 🚨 do NOT do this
// ✅ The fix: make the data outlive the view. Keep an owner around,
// or return an owning std::string instead of a view.
string owner = "I stay alive";
string_view ok = owner; // owner outlives ok -> safe
cout << ok << endl; // I stay alive
// Another trap: mutating the owner can REALLOCATE its buffer,
// leaving the view pointing at freed memory:
string buf = "abc";
string_view v = buf;
// buf += "def..."; // 🚨 may reallocate -> v now dangles
cout << v << endl; // abc (safe only because buf is untouched)
return 0;
}
// ✅ Expected output:
// I stay alive
// abcNo null terminator: a string_view can point at a slice in the middle of a buffer, so there is no guaranteed trailing \0. Never hand view.data() to a C API that expects a null-terminated string — build a std::string from the view first and use its .c_str().
4. std::span — The Array Analogue
std::span<T> (C++20) takes the exact same idea — a non-owning pointer plus length — and applies it to any contiguous sequence: the elements of a std::vector, a std::array, or a raw C buffer. A function taking span<const int> accepts all of them without copying, just as string_view does for characters. The same caveat applies: a span borrows, so it must not outlive its data, and subspan only adjusts the window.
#include <iostream>
#include <vector>
#include <span> // C++20
using namespace std;
// std::span<T> is "string_view for arrays": a non-owning pointer+length
// view over any contiguous sequence -- a vector, an array, a C buffer.
int sum(span<const int> values) {
int total = 0;
for (int v : values) total += v;
return total;
}
int main() {
vector<int> v = {1, 2, 3, 4};
int raw[] = {10, 20, 30};
cout << "vector sum: " << sum(v) << endl; // vector sum: 10
cout << "array sum: " << sum(raw) << endl; // array sum: 60
// Like string_view, a span borrows -- it must not outlive its data,
// and operations like subspan only adjust the window, never copy.
span<const int> tail = span<const int>(v).subspan(2); // {3, 4}
cout << "tail sum: " << sum(tail) << endl; // tail sum: 7
return 0;
}
// ✅ Expected output:
// vector sum: 10
// array sum: 60
// tail sum: 7🔎 Deep Dive: own vs borrow, and how to convert
Borrow by default, own when you must outlive the source. Use a string_view parameter for read-only text that you only need during the call. The moment you need to store the characters past the lifetime of the source — in a member, a container, or a returned value — copy them into an owning std::string.
Conversion goes both ways and is explicit in the dangerous direction: a std::string converts to a string_view implicitly (cheap, borrowing), but turning a string_view back into an owning string requires you to write std::string(sv) — a deliberate copy that makes the ownership transfer visible.
void use(string_view sv); // borrow: fine for the duration of the call
struct Cache { string text; }; // OWN: must copy in
Cache make(string_view sv) {
return Cache{ string(sv) }; // copy the viewed chars into an owner
}
// sv may dangle after this; the Cache's std::string is safe to keepPro Tips
- 💡 Pass string_view by value: it is just a pointer and a length, so copying it is free.
- 💡 Default to string_view for read-only text params — it avoids needless temporaries from literals.
- 💡 Never store a view of a temporary: if you must keep the data, copy it into a std::string.
- 💡 Don't assume null-termination: use .size(), not C-string functions, on view data.
🧠 Quick Recall
Predict the result before revealing the answer.
1. Is this safe?
string_view f() {
string local = "hi";
return local;
}No — local is destroyed when f returns, so the view dangles. Return an owning std::string instead.
2. How many copies does this make?
string_view v = "Hello, world";
string_view w = v.substr(0, 5);Zero — substr on a view just shifts the pointer and length; no characters are copied.
3. What can go wrong here?
string s = "abc";
string_view v = s;
s += "defghijklmnop";
cout << v;The append may reallocate s, leaving v pointing at freed memory — undefined behaviour. Don't mutate the owner while a view is alive.
Common Errors (and the fix)
- Returning a view of a local: the local string dies on return, so the view dangles. Return a std::string by value instead.
- Storing a view of a temporary: string_view v = getString(); where getString returns by value leaves v dangling at the semicolon. Keep an owner alive or copy into a std::string.
- Mutating the owner: appending to the std::string a view points at can reallocate it, invalidating the view. Don't modify the source while the view is in use.
- Assuming null-termination: passing view.data() to printf("%s") or fopen reads past the view. Build a std::string and use .c_str().
- Trying to modify through a view: string_view is read-only and has no push_back or write access. Use the owning std::string for mutation.
📋 Quick Reference
| Task | Code |
|---|---|
| View a string | string_view v = s; |
| Read-only param | void f(string_view t) |
| Zero-copy slice | v.substr(0, 5) |
| Trim the window | v.remove_prefix(2) |
| Length | v.size() |
| Copy to owner | string s(v); |
| Array view | span<const int> a = vec; |
Mini-Challenge: Count Words Without Copying
Write countWords(string_view) that counts space-separated words by walking the view with find and remove_prefix — never building a std::string, never copying.
#include <iostream>
#include <string>
#include <string_view>
using namespace std;
// 🎯 MINI-CHALLENGE: count words without copying
// 1. Write int countWords(string_view text) that returns how many
// space-separated words are in text.
// 2. Walk the view with find(' ') and remove_prefix to skip past each
// word + space -- never build a std::string, never copy.
// 3. In main, call it on a sentence and a string literal.
//
// ✅ Example: countWords("one two three") == 3
// your code here🎉 Lesson Complete
- ✅ A string_view is a non-owning pointer + length over existing characters
- ✅ Substrings and trims are zero-copy — pass views by value
- ✅ Prefer string_view to const std::string& for read-only text params
- ✅ A view must never outlive its data, and the owner must not be reallocated under it
- ✅ A view is not guaranteed null-terminated — copy to a std::string for C APIs
Practice quiz
What does a std::string_view actually store?
- A pointer to char data plus a length
- A full owning copy of the characters
- A reference count and a buffer
- A null-terminated C string only
Answer: A pointer to char data plus a length. A string_view is just a pointer and a length — a lightweight, non-owning window over character data that lives elsewhere.
Why is taking a substring of a string_view cheap?
- It uses move semantics on the buffer
- It compresses the data
- It only adjusts the pointer and length — no characters are copied
- It caches the result
Answer: It only adjusts the pointer and length — no characters are copied. substr on a view returns another view by shifting the pointer and shrinking the length; no allocation or copy happens.
Which parameter type lets a function accept both std::string and string literals without copying?
- std::string by value
- std::string_view
- char*
- const std::string&
Answer: std::string_view. A std::string_view parameter binds to std::string, string literals, and char arrays alike, all without an allocation or copy.
What is the main danger of std::string_view?
- It is always slower than std::string
- It cannot be passed to functions
- It copies too much memory
- It can dangle if the data it views is destroyed before the view
Answer: It can dangle if the data it views is destroyed before the view. A view does not own its data, so if the underlying string is destroyed or reallocated, the view becomes a dangling reference.
Does std::string_view guarantee its data is null-terminated?
- No — a view may point into the middle of a larger buffer with no terminator
- Only if created from a literal
- Only on Windows
- Yes, always
Answer: No — a view may point into the middle of a larger buffer with no terminator. A view can reference a slice of a buffer, so there is no guaranteed '\0' at the end; never pass view.data() to C APIs expecting a C string.
Which of these is a SAFE use of string_view?
- Storing a view of a temporary that has already been destroyed
- Reading a substring of a string that stays alive for the whole call
- Viewing a string and then resizing that string
- Returning a view of a local std::string from a function
Answer: Reading a substring of a string that stays alive for the whole call. A view is safe as long as the data it points at outlives the view and is not reallocated; reading a stable string during a call is fine.
What is the array analogue of string_view in C++20?
- std::list
- std::vector
- std::array
- std::span<T>
Answer: std::span<T>. std::span<T> is a non-owning view (pointer + length) over a contiguous sequence of any element type — string_view for arrays.
What happens to a std::string_view's contents if you call push_back on the std::string it views?
- The view updates automatically
- Nothing can go wrong
- The string may reallocate, leaving the view dangling
- The view grows too
Answer: The string may reallocate, leaving the view dangling. Appending can force the string to reallocate its buffer; the old pointer the view held is then invalid — a classic dangling bug.
Why prefer string_view over const std::string& for a read-only parameter?
- It owns the data more safely
- It avoids forcing callers with a literal or char array to build a temporary std::string
- It is null-terminated
- It allows modifying the string
Answer: It avoids forcing callers with a literal or char array to build a temporary std::string. A const std::string& parameter forces a literal or char array to be converted into a temporary std::string; a string_view binds directly with no allocation.
Which operation is NOT available on std::string_view?
- push_back
- substr
- size
- remove_prefix
Answer: push_back. A view is read-only and non-owning, so it cannot modify or grow the underlying data; push_back belongs to std::string, not string_view.
Continue this course
- Previous: optional, variant & any (C++17)
- Next: Error Handling with expected (C++23) — Return a value or an error without exceptions, with monadic and_then/transform
- Quick reference: C++ cheat sheet › Strings & I/O
Frequently asked questions
What exactly is std::string_view?
It is a lightweight, non-owning view over a sequence of characters — internally just a pointer to the first character and a length. It does not allocate, does not copy, and does not own the data; it merely describes a window into characters that live somewhere else (a std::string, a string literal, a char array). Because it is so cheap to copy, you pass it by value.
When should I use std::string_view as a function parameter?
Use it for read-only string parameters. A const std::string& parameter forces callers who pass a string literal or a char array to materialise a temporary std::string, which allocates. A std::string_view binds directly to std::string, literals, and char arrays with no allocation and no copy, while still giving you size(), substr(), and the usual read operations. Switch to const std::string& only when the function genuinely needs an owning string.
What is the biggest danger with string_view?
Dangling. A view does not keep its data alive, so if the underlying buffer is destroyed — or reallocated, for example by appending to the std::string it views — the view points at freed or moved memory and using it is undefined behaviour. Never return a string_view that refers to a local string, never store a view of a temporary, and never mutate the owner while a view of it is in use.
Is a string_view null-terminated?
No. A view can reference a slice in the middle of a larger buffer, so there is no guarantee of a trailing '\0'. That means you must not pass view.data() to C functions that expect a null-terminated C string (like printf %s or fopen). If you need a real C string, build a std::string from the view first and call .c_str() on that.
How does std::span relate to string_view?
std::span<T> (C++20) is the same idea generalised to any element type: a non-owning pointer-plus-length view over a contiguous range, such as the elements of a std::vector or a C array. Where string_view is the read-only window over characters, span is the window over arrays. span can also be mutable, letting you read and write the underlying elements without owning them.