1. Introduction
If you have ever copied a string "just to pass it to a function" — you are not alone. People often do that by inertia until they start tracking who actually stores the data.
Today we'll discuss a simple question: who is responsible for the lifetime of the string characters. And we'll find out why sometimes "just passing a string" suddenly means "copying half a novel".
Imagine we have a function that "just prints a string":
#include <iostream>
#include <string>
void print_message(std::string s) { // <- accept by value (copy)
std::cout << s << '\n';
}
int main() {
std::string msg = "Hello";
print_message(msg);
}
On a semantic level everything is fine: we passed the text and the function printed it. But at the action level the program might have done unnecessary work: it created a copy of msg inside print_message(). Sometimes this is harmless (the string is short), and sometimes it's noticeable (the string is large, or there are many calls).
Here a useful thought arises: if a function does not intend to own the string, then maybe it does not need a copy. It only needs access to the characters.
2. std::string — the data owner
To understand how std::string_view differs from std::string, first let's respectfully shake hands with std::string itself: it's not "just an array of chars", but a smart object with responsibilities.
std::string owns its characters: it manages memory and is responsible for ensuring that the data exists while the string object lives. And yes, this sometimes means allocating memory and copying.
Simply put (very simplified, without low-level details), std::string has "metadata" and somewhere a "buffer with characters". Inside the object it stores the length, usually also capacity, and a reference/pointer to where the characters live. The key point — this buffer belongs to the string: when the string is destroyed, its characters disappear too.
Think of it as a box with stuff. While the box is at your home — the stuff is available. Throw the box out — access is gone.
Mini example "string lifetime inside a block":
#include <iostream>
#include <string>
int main() {
std::string s = "Alive";
std::cout << s << '\n'; // Alive
} // <- s is destroyed here, together with ownership of the data
One more important detail (you've already seen it partially with std::vector): sometimes a string is modified so that it needs more space and it may "move" to a new buffer. This is not a bug but a normal optimization mechanism. We won't dive into invalidation rules now, but note the fact: the owner has the right to change its internal buffer.
3. std::string_view — a view on data
Now let's imagine the opposite situation: you don't need the "box", you just need to quickly look at what is written without taking the text for yourself. That's exactly what std::string_view exists for.
It is a "view" type: it does not own the characters and is usually implemented as "pointer to characters + length". To be blunt: std::string_view is not an owning string but a "label with an address and a count of characters". It does not allocate memory for text and does not copy characters when you create it from existing data.
Simple example:
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string s = "Hello";
std::string_view v = s; // v views data inside s
std::cout << s << '\n'; // Hello
std::cout << v << '\n'; // Hello
}
Here v is a "view" of s's contents. It's lightweight, cheap to copy, and it is perfect for "read and forget".
It's useful to compare std::string and std::string_view in a table:
| Property | |
|
|---|---|---|
| Owns characters | Yes | No |
| Can store a result "permanently" | Yes (while the object lives) | No (only while the foreign data lives) |
Creation from |
May copy | Usually does not copy |
| Suitable for modifying text | Yes | No (conceptually read-only access) |
| Typical scenario | "I store the text" | "I read the text" |
In the standard you will find many places where interfaces purposely accept std::string_view to avoid unnecessary copies and make APIs more flexible.
How a view "looks" at an owner
Imagine there is one text and two objects next to it. One object is the owner (std::string), the other is the observer (std::string_view). The observer does not make a copy and does not become a "second owner": it just remembers where the owner's characters live and how many to consider.
Diagram that helps avoid confusion:
flowchart LR
S[std::string s
owns data] --> B[(character buffer)]
V[std::string_view v
does not own] --> B
While s is alive and while its data is at the place where v expects, v correctly shows the string.
An interesting effect: if you change a character in std::string, the view will "see" that change because it looks at the same data:
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string s = "hello";
std::string_view v = s;
s[0] = 'H';
std::cout << s << '\n'; // Hello
std::cout << v << '\n'; // Hello
}
Sounds convenient, but it's also a hint for future caution: the view does not protect you from the owner's actions. If the owner decides it needs a new buffer (for example, the string grows a lot), the view may suddenly be "looking at the old address". The basic rule is simple: the view lives according to the owner's rules, not the other way around.
What std::string_view can and cannot do
At this point a natural desire arises: "Oh, great! Let's always use std::string_view instead of std::string — and everything will be free and fast." That's roughly like deciding to live on sauce only because it's tasty. The sauce is good, but it doesn't replace the meal.
std::string_view is good precisely as a type for temporary access. It can do things that make sense for "viewing": get length, check emptiness, access a character by index, compare with another string/view, stream it out. But it is not meant for "accumulating text", "appending pieces", or "storing the result".
Small example of basic operations:
#include <iostream>
#include <string_view>
int main() {
std::string_view v = "abc";
std::cout << v.size() << '\n'; // 3
std::cout << v[0] << '\n'; // a
}
If you need to obtain an owning copy (for example, to keep the result for a long time), you go back to std::string:
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string_view v = "Hi";
std::string owned(v); // copy characters
std::cout << owned << '\n'; // Hi
}
One more nuance that beginners often miss: string_view is "pointer + length", not necessarily a C-style string with a terminating '\0'. It can view a slice inside a string, a slice inside a character array, and is not required to be null-terminated in the usual sense.
So the idea "I'll take v.data() and it will work everywhere like a C-string" is dangerous. We won't dive into the C API here; just remember: a view promises fast access to a range of characters, but not the convenience "like a C-string".
4. Example: no-copy command handling in a console app
So that the topic doesn't remain "pointer theory", let's connect it to a simple console application. Suppose it's a tiny command interpreter: the program reads a line and then decides what to do — for example, exit, show help, or report an unknown command.
Today's modest goal: learn to "view" the input line without unnecessary copies.
Important: we are not doing complex parsing here or splitting the line into arguments. Here we practice the basic mechanic: "there is an owning string and there is a view on it".
#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string line;
while (std::getline(std::cin, line)) {
std::string_view sv = line; // sv lives only inside the iteration
if (sv == "exit") {
std::cout << "Bye!\n"; // Bye!
break;
}
if (sv == "help") {
std::cout << "Commands: help, exit\n"; // Commands: help, exit
continue;
}
std::cout << "Unknown: " << sv << '\n'; // Unknown: ...
}
}
Note the style: std::string_view sv = line; is created next to its use and lives briefly — exactly as long as line does in that loop iteration. This is the "healthy" scenario for which string_view was designed: quickly inspect, compare, decide.
If you're wondering now: "Can I pass std::string_view to functions instead of a const std::string&?" — yes, and that's the next logical step (but let's not get ahead). The important thing is to clearly separate roles in your mind: line stores, sv looks.
5. Common mistakes when working with std::string_view
Mistake #1: assuming std::string_view stores a copy of the text.
This is the most common trap: you see a "string" type, it prints via std::cout, it compares, it has size() — the brain automatically thinks "well, it's a string". But it's not an owning string. It's a "tag" on someone else's characters. So if the owner disappears, the view does not magically remain valid: it becomes a very confident pointer to nowhere.
Mistake #2: returning a std::string_view that points to a local std::string.
Sometimes people want a "convenient" function that returns a view because it's "fast". But if you create a local string inside the function, it will be destroyed upon function exit. The view will remain, but the data will be gone. Even if "it seems to work" on small tests, this is exactly the case where the program simply hasn't failed you yet.
#include <string>
#include <string_view>
std::string_view bad() {
std::string s = "local";
return std::string_view{s}; // s will die after return
}
Mistake #3: trying to "edit" text via a view.
std::string_view is conceptually not about editing. It does not give you push_back(), +=, replace() and other joys of the builder crew. If you need to modify text — modify the owner (std::string) or create a new std::string and own the result.
Mistake #4: indexing with operator[] without bounds checking.
std::string_view has operator[], and it does not check bounds (just like std::string). So expression sv[0] on an empty string is a direct ticket to undefined behavior. If an index comes from outside (for example, from a user), first think about empty() and size().
Mistake #5: making a view "long-lived" out of habit.
Beginners sometimes create a std::string_view current; "somewhere near the top", assign different strings to it over time, and then wonder why after a few steps there's garbage. A view is good when it lives briefly: create → use → forget. The longer a view lives, the harder it is to guarantee that the owner is still alive and that its buffer hasn't changed.
GO TO FULL VERSION