Advanced std::string_view Techniques
Avoid common pitfalls with string_view lifetimes and data ownership.
What Is a Dangling std::string_view?
A std::string_view never holds any characters of its own. It points at characters that belong to something else. If that something else disappears, or moves its characters somewhere new, the view is left pointing at memory that no longer means anything. A view in that state is called a dangling view, and reading from it is undefined behavior.
That single fact is the whole subject of this lesson. Everything below is either a way to create a dangling view by accident, or a rule for making sure you never do.
What a View Actually Holds
The previous lesson described std::string_view as "read-only access to an existing string". Mechanically, a std::string_view is two small values: the address of the first character it is looking at, and how many characters it is looking at.
std::string code owns [ E G Y - 1 9 2 1 - A L A B A S T E R \0 ]
^
std::string_view codeView address of this character, plus length 18
Copying a std::string_view copies those two values, which is why views are cheap. But copying an address does not copy what the address points to. The view is a note that says "the characters are over there", and it has no way to check whether "over there" is still valid.
A `std::string_view` stores where the characters are, not the characters themselves. Nothing in the view can detect that the characters have gone away.
std::string Owns Its Characters
std::string takes the opposite approach. When you initialize a std::string, it allocates its own storage and copies the characters into it. From that moment on, it is independent of whatever it was initialized from.
#include <iostream>
#include <string>
int main()
{
std::string original { "Bronze astrolabe" };
std::string archived { original }; // archived copies the characters into its own storage
original = "Carved ivory comb"; // changing original after the copy was made
std::cout << "original: " << original << '\n';
std::cout << "archived: " << archived << '\n';
return 0;
}
Output:
original: Carved ivory comb
archived: Bronze astrolabe
archived is unaffected by anything that happens to original. That independence is what we mean when we call std::string an owner: it acquires the characters, it is the one place responsible for them, and it releases them when it is destroyed. The price of that independence is the copy, which is exactly the cost std::string_view exists to avoid.
An owner keeps its own copy and answers for it. A viewer keeps a reference to someone else's copy and answers for nothing. Ownership costs a copy and buys independence; viewing is free and buys nothing.
std::string_view Borrows Them
Because a view costs nothing to create, a std::string_view parameter is the standard way to accept read-only text. It accepts every string type the caller might have without copying any of them.
#include <iostream>
#include <string>
#include <string_view>
void describeExhibit(std::string_view caption) // views whatever the caller passed in
{
std::cout << "Label: " << caption << '\n';
}
int main()
{
describeExhibit("Basalt fragment"); // a C-style string literal
std::string plate { "Bronze astrolabe" };
describeExhibit(plate); // a std::string
std::string_view borrowed { plate };
describeExhibit(borrowed); // another std::string_view
return 0;
}
Output:
Label: Basalt fragment
Label: Bronze astrolabe
Label: Bronze astrolabe
This is safe by construction. The parameter is created when the call starts and destroyed when the call ends, and the argument in the caller cannot go anywhere in between.
A `std::string_view` function parameter is the safest place to use a view, and should be your default for read-only string parameters.
The "read-only" half of std::string_view is enforced by the compiler. Try to write through a view and the program will not build. The example below is deliberately broken.
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string plate { "Bronze astrolabe" };
std::string_view caption { plate };
caption[0] = 'b'; // a view is read-only, so this does not compile
std::cout << caption << '\n';
return 0;
}
s.cpp: In function 'int main()':
s.cpp:10:16: error: assignment of read-only location 'caption.std::basic_string_view<char>::operator[](0)'
10 | caption[0] = 'b'; // a view is read-only, so this does not compile
Note carefully what the compiler did and did not do for you. It stopped you writing through a view. It cannot stop you reading through a dead one. Every broken example in the rest of this lesson compiles without a single warning under -Wall -Wextra.
Dangling views are not diagnosed. A program that reads one may print the right answer today, print garbage tomorrow, and crash in production. You are responsible for lifetimes, not the compiler.
The Only Question That Matters: What Outlives What?
Before you write any std::string_view, ask one question: will the characters still be alive and unchanged everywhere this view is used? The answer depends entirely on what you initialized the view from.
| Initialize the view from | The characters live | Verdict |
|---|---|---|
A C-style string literal, "EGY-1921" |
for the entire program | always safe |
A std::string_view literal, "EGY-1921"sv |
for the entire program | always safe |
A std::string variable |
until that variable is destroyed or modified | safe while the variable outlives the view and stays untouched |
Another std::string_view |
as long as whatever that view points at | inherits that lifetime |
A std::string returned by value from a function |
to the end of the current statement | dangles from the next statement onwards |
A std::string literal, "EGY-1921"s |
to the end of the current statement | dangles from the next statement onwards |
The last two rows are the ones that catch people out, because nothing about the code looks temporary.
Four Ways to End Up Dangling
Each of the four patterns below is broken on purpose. Nothing a broken program prints can be trusted, so none of them is shown with output. Where the repair is worth seeing, a working version follows.
1. The owner goes out of scope
A pair of braces creates a nested block, and anything declared inside it is destroyed at the closing brace. A view declared outside the block survives it, and loses whatever it was pointing at. The following example is broken.
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string_view codeView {};
{ // a nested block: whatever is declared here dies at the closing brace
std::string code { "EGY-1921-ALABASTER" };
codeView = code; // codeView now points at code's characters
} // code is destroyed here, and codeView is left dangling
std::cout << codeView << '\n'; // undefined behavior: reading a dangling view
return 0;
}
codeView outlives code, which is precisely the wrong way round. By the time it is printed it holds the address of storage that has already been released.
2. The owner is a temporary returned by a function
A function that returns a std::string by value hands back a temporary object. Temporaries are destroyed at the end of the statement that created them. The following example is broken.
#include <iostream>
#include <string>
#include <string_view>
std::string buildAccessionCode()
{
return std::string { "EGY-1921-ALABASTER" };
}
int main()
{
std::string_view codeView { buildAccessionCode() }; // views a temporary std::string
std::cout << codeView << '\n'; // undefined behavior: the temporary is already gone
return 0;
}
Had codeView been a std::string, it would have copied the characters out of the temporary before the temporary died. A view copies nothing, so when the temporary is destroyed at the semicolon there is nothing left to look at.
The fix is to give the characters an owner that lives long enough:
#include <iostream>
#include <string>
#include <string_view>
std::string buildAccessionCode()
{
return std::string { "EGY-1921-ALABASTER" };
}
int main()
{
std::string code { buildAccessionCode() }; // code owns the characters
std::string_view codeView { code }; // codeView borrows from code, which outlives it
std::cout << codeView << '\n';
return 0;
}
Output:
EGY-1921-ALABASTER
3. The initializer is a std::string literal
The s suffix does not produce a literal that sits in the program's static storage. It builds a std::string on the spot, and that std::string is a temporary. This is case 2 wearing a disguise, and the following example is broken.
#include <iostream>
#include <string>
#include <string_view>
int main()
{
using namespace std::string_literals;
std::string_view codeView { "EGY-1921-ALABASTER"s }; // the s suffix builds a temporary std::string
std::cout << codeView << '\n'; // undefined behavior
return 0;
}
Drop the s and the same line is perfectly safe, because a plain "EGY-1921-ALABASTER" is a C-style string literal that exists for as long as the program runs. The sv suffix is safe too, since a std::string_view literal is a C-style string literal underneath.
Never initialize a `std::string_view` with a `std::string` literal (the `s` suffix). The temporary it creates is destroyed at the end of the statement and leaves the view dangling. Use no suffix, or the `sv` suffix.
4. The owner is modified while the view is alive
This one is different from the other three. The owner is still alive. It has simply changed, and the view did not come along for the ride. The following example is broken.
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string code { "EGY-1921" };
std::string_view codeView { code }; // codeView points at code's characters
code = "PER-1953-TURQUOISE"; // code may move its characters to a larger buffer
std::cout << codeView << '\n'; // undefined behavior: codeView was invalidated
return 0;
}
A view in this state is described as invalidated. To see why an assignment can be enough to break it, look at how much room a std::string actually has. The following program is well defined, so we can run it:
#include <iostream>
#include <string>
int main()
{
std::string code { "EGY-1921" };
std::cout << "buffer size: " << code.capacity() << '\n';
code = "PER-1953-TURQUOISE-BEADS-CABINET-SEVEN";
std::cout << "buffer size: " << code.capacity() << '\n';
return 0;
}
Output:
buffer size: 15
buffer size: 38
capacity() reports how many characters the current buffer can hold. The buffer that held 15 characters could not hold 38, so the std::string had to allocate a new one and release the old one. Any view still pointing into the old buffer is now pointing at released memory.
Even when no reallocation happens, a view can still be wrong. The new characters land at the same address, but the view is still carrying the old length, so it ends up showing a truncated piece of the new text or running off the end of it. Either way the result is not something you can rely on.
Modifying a `std::string` invalidates every view into it, whether or not the characters moved. Treat any change to an owner as destroying all views of it.
Repairing an Invalidated View
An invalidated view is not permanently ruined. Assigning it a valid string to view puts it back into a usable state, because the assignment refreshes both the address and the length. The rule is simply that you must repair it before you read it, not after.
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string code { "EGY-1921" };
std::string_view codeView { code };
std::cout << codeView << '\n'; // fine: code has not been touched yet
code = "PER-1953-TURQUOISE"; // this assignment invalidates codeView
codeView = code; // revalidate it before reading it again
std::cout << codeView << '\n';
return 0;
}
Output:
EGY-1921
PER-1953-TURQUOISE
Returning a std::string_view
A function may return a std::string_view, but only when the characters it points at will still be alive in the caller. Returning a view of a local std::string fails that test, because locals are destroyed on the way out. The following example is broken.
#include <iostream>
#include <string>
#include <string_view>
std::string_view exhibitStatus(bool onDisplay)
{
std::string shown { "On display" }; // local std::string
std::string stored { "In storage" }; // local std::string
if (onDisplay)
return shown;
return stored;
} // shown and stored are destroyed here, so the returned view dangles
int main()
{
std::cout << exhibitStatus(true) << '\n'; // undefined behavior
return 0;
}
There are two situations where returning a view is genuinely fine.
Returning a C-style string literal. Literals live for the whole program, so a view of one is valid everywhere. Removing the two local std::string objects from the previous function is all it takes:
#include <iostream>
#include <string_view>
std::string_view exhibitStatus(bool onDisplay)
{
if (onDisplay)
return "On display";
return "In storage";
} // the two literals are not destroyed here: they live for the whole program
int main()
{
std::cout << exhibitStatus(true) << '\n';
std::cout << exhibitStatus(false) << '\n';
return 0;
}
Output:
On display
In storage
Returning a std::string_view parameter. A view parameter already points at something owned by the caller, so handing it straight back hands the caller a view of its own data.
#include <iostream>
#include <string>
#include <string_view>
std::string_view shorterCaption(std::string_view first, std::string_view second)
{
if (first.size() <= second.size())
return first;
return second;
}
int main()
{
std::string plate { "Basalt fragment" };
std::string label { "Carved ivory comb" };
std::cout << shorterCaption(plate, label) << '\n';
return 0;
}
Output:
Basalt fragment
plate and label are still alive in main() when the returned view is printed, so there is nothing to dangle.
That guarantee comes from the caller, though, not from the function. If the caller passes a temporary, the returned view is only good until the end of that statement. The following example is broken.
#include <iostream>
#include <string>
#include <string_view>
std::string buildAccessionCode()
{
return std::string { "EGY-1921-ALABASTER" };
}
std::string_view shorterCaption(std::string_view first, std::string_view second)
{
if (first.size() <= second.size())
return first;
return second;
}
int main()
{
// shorterCaption returns a view into the temporary that buildAccessionCode() produced
std::string_view picked { shorterCaption(buildAccessionCode(), "PER-1953-TURQUOISE-BEADS") };
std::cout << picked << '\n'; // undefined behavior: that temporary died at the end of the line above
return 0;
}
Consuming the result inside the same statement is fine, because the temporary is still alive until that statement finishes:
#include <iostream>
#include <string>
#include <string_view>
std::string buildAccessionCode()
{
return std::string { "EGY-1921-ALABASTER" };
}
std::string_view shorterCaption(std::string_view first, std::string_view second)
{
if (first.size() <= second.size())
return first;
return second;
}
int main()
{
// the temporary lives until the end of this statement, so printing here is safe
std::cout << shorterCaption(buildAccessionCode(), "PER-1953-TURQUOISE-BEADS") << '\n';
return 0;
}
Output:
EGY-1921-ALABASTER
If any argument to a function returning `std::string_view` is a temporary, use the returned view within the same statement. Storing it in a variable leaves it dangling.
Narrowing a View Without Copying
Because a view is only an address and a length, it can be adjusted to cover less of the string it points at. Two member functions do exactly that:
remove_prefix(n)advances the address byncharacters, shrinking the view from the leftremove_suffix(n)reduces the length byncharacters, shrinking the view from the right
Neither one touches the characters being viewed. They only change the two numbers inside the view, which makes them useful for pulling a field out of a structured code.
#include <iostream>
#include <string_view>
int main()
{
std::string_view codeView { "EGY-1921-ALABASTER" };
std::cout << codeView << '\n';
codeView.remove_prefix(4); // hide the four characters "EGY-" on the left
std::cout << codeView << '\n';
codeView.remove_suffix(10); // hide the ten characters "-ALABASTER" on the right
std::cout << codeView << '\n';
codeView = "EGY-1921-ALABASTER"; // reassigning is the only way to widen the view again
std::cout << codeView << '\n';
return 0;
}
Output:
EGY-1921-ALABASTER
1921-ALABASTER
1921
EGY-1921-ALABASTER
Notice the last step. remove_prefix() and remove_suffix() are one-way: a view cannot grow back on its own, because it has forgotten where the rest of the string was. Assigning it a source string again is the only way to widen it.
What these functions produce is a substring, meaning a run of characters that appear next to each other inside a larger string. In "EGY-1921-ALABASTER", the sequences "1921", "ALAB", and "STER" are substrings; "EGYPT" is not, because those characters do not sit side by side in that order.
substr() gives you the same result without disturbing the original view. It takes a starting position and a count, and returns a new view of that slice:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
constexpr std::string_view fullCode { "EGY-1921-ALABASTER" };
std::string_view year { fullCode.substr(4, 4) }; // views 4 characters inside fullCode
std::cout << "view: " << year << '\n';
std::string yearText { year }; // owns its own null-terminated copy of those characters
std::cout << "copy: " << yearText << '\n';
return 0;
}
Output:
view: 1921
copy: 1921
Why You Cannot Assume a Null Terminator
Substring views have one consequence worth spelling out. In the program above, year points at the four characters 1921 sitting in the middle of fullCode. The character stored immediately after the final 1 is a -, not a null character. Nothing marks where the view ends except the length the view is carrying.
A C-style string literal and a `std::string` are always null-terminated. A `std::string_view` may or may not be.
This is almost never a problem, because a view knows its own length and std::cout uses that length rather than hunting for a terminator. It matters only when you hand the characters to something that does expect a terminator, such as an older C-style interface.
Never write code that assumes a `std::string_view` is null-terminated. When you need a guaranteed null-terminated string, construct a `std::string` from the view, as `yearText` does above. The `std::string` copies the characters and adds the terminator.
Picking Between std::string and std::string_view
The choice follows from ownership. If your code needs the characters to stick around or to change, it needs an owner. If it only needs to read characters that someone else is already keeping alive, a view will do.
| What you need | Use |
|---|---|
| Text you will modify | std::string |
| Text read from user input | std::string |
To keep the result of a function that returns std::string |
std::string |
| A read-only string parameter | std::string_view |
| Read-only access to text an outliving owner already holds | std::string_view |
| A named string constant | constexpr std::string_view |
| To return a string literal from a function | std::string_view |
| To return a string the function built itself | std::string, returned by value |
| To pass text to code that requires a null-terminated C string | std::string or const std::string& |
For function parameters specifically, prefer std::string_view over const std::string& in most cases. The exception is a function that forwards its argument to something needing a real std::string, where the view would just have to be converted back.
Summary
- A
std::string_viewstores an address and a length, never the characters. That is why it is cheap, and why it cannot tell when the characters are gone std::stringis a sole owner: it copies the characters into storage it controls, so it is independent of whatever it was initialized from- A view whose characters have been destroyed is dangling; a view whose characters have changed is invalidated. Reading either one is undefined behavior
- The compiler enforces that a view is read-only, but it does not diagnose dangling or invalidated views. Lifetimes are your responsibility
- Views of C-style string literals and
svliterals are always safe, because literals live for the whole program - Never initialize a view from a
std::stringliteral ("text"s) or from astd::stringreturned by value. Both create temporaries that die at the end of the statement - Modifying a
std::stringinvalidates every view into it, because the characters may move to a larger buffer or change length in place - Assigning a valid string to an invalidated view revalidates it, as long as you do so before reading it
- Returning a
std::string_viewis safe for C-style string literals and forstd::string_viewparameters, and unsafe for local variables. If any argument was a temporary, use the returned view within the same statement remove_prefix(),remove_suffix(), andsubstr()shrink the view, not the string. Only reassignment can widen a view again- A
std::string_viewmay or may not be null-terminated. Build astd::stringfrom it when a terminator is required - Reach for
std::stringwhen you need to own or change text, andstd::string_viewwhen you only need to read text that something else is keeping alive
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Advanced std::string_view Techniques - Quiz
Test your understanding of the lesson.
Practice Exercises
String View Operations
Use std::string_view for efficient string processing without copying.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!