In prior lessons, we introduced two string types: std::string
(5.7 -- Introduction to std::string) and std::string_view
(5.8 -- Introduction to std::string_view).
Because std::string_view
is our first encounter with a view type, we’re going to spend some additional time discussing it further. We will focus on how to use std::string_view
safely, and provide some examples illustrating how it can be used incorrectly. We’ll conclude with some guidelines on when to use std::string
vs std::string_view
.
An introduction to owners and viewers
Let’s sidebar into an analogy for a moment. Say you’ve decided that you’re going to paint a picture of a bicycle. But you don’t have a bicycle! What are you to do?
Well, you could go to the local cycle shop and buy one. You would own that bike. This has some benefits: you now have a bike that you can ride. You can guarantee the bike will always be available when you want it. You can decorate it, or move it. There are also some downsides to this choice. Bicycles are expensive. And if you buy one, you are now responsible for it. You have to periodically maintain it. And when you eventually decide you don’t want it any more, you have to properly dispose of it.
Ownership can be expensive. As an owner, it is your responsibility to acquire, manage, and properly dispose of the objects you own.
On your way out of the house, you glance out your window front. You notice that your neighbor has parked their bike across from your window. You could just paint a picture of your neighbor’s bike (as seen from your window) instead. There are lots of benefits to this choice. You save the expense of having to go acquire your own bike. You don’t have to maintain it. Nor are you responsible for disposing of it. When you are done viewing, you can just shut your curtains and move on with your life. This ends your view of the object, but the object itself is not affected by this. There are also some potential downsides to this choice. You can’t paint or customize your neighbors bike. And while you are viewing the bike, your neighbor may decide to change the way the bike looks, or move it out of your view altogether. You may end up with a view of something unexpected instead.
Viewing is inexpensive. As a viewer, you have no responsibility for the objects you are viewing, but you also have no control over those objects.
std::string
is an owner
You might be wondering why std::string
makes an expensive copy of its initializer. When an object is instantiated, memory is allocated for that object to store whatever data it needs to use throughout its lifetime. This memory is reserved for the object, and guaranteed to exist for as long as the object does. It is a safe space. std::string
(and most other objects) copy the initialization value they are given into this memory so that they can have their own independent value to access and manipulate later. Once the initialization value has been copied, the object is no longer reliant on the initializer in any way.
And that’s a good thing, because the initializer generally can’t be trusted after initialization is complete. If you imagine the initialization process as a function call that initializes the object, who is passing in the initializer? The caller. When initialization is done, control returns back to the caller. At this point, the initialization statement is complete, and one of two things will typically happen:
- If the initializer was a temporary value or object, that temporary will be destroyed immediately.
- If the initializer was a variable, the caller still has access to that object. The caller can then do whatever it wants with the object, including modify or destroy it.
Key insight
An initialized object has no control over what happens to the initialization value after initialization is finished.
Because std::string
makes its own copy of the value, it doesn’t have to worry about what happens after initialization is finished. The initializer can be destroyed, or modified, and it doesn’t affect the std::string
. The downside is that this independence comes with the cost of an expensive copy.
In the context of our analogy, std::string
is an owner. It has its own data that it manages. And when it is destroyed, it cleans up after itself.
We don’t always need a copy
Let’s revisit this example from the prior lesson:
#include <iostream>
#include <string>
void printString(std::string str) // str makes a copy of its initializer
{
std::cout << str << '\n';
}
int main()
{
std::string s{ "Hello, world!" };
printString(s);
return 0;
}
When printString(s)
is called, str
makes an expensive copy of s
. The function prints the copied string and then destroys it.
Note that s
is already holding the string we want to print. Could we just use the string that s
is holding instead of making a copy? The answer is possibly -- there are three criteria we need to assess:
- Could
s
be destroyed whilestr
is still using it? No,str
dies at the end of the function, ands
exists in the scope of the caller and can’t be destroyed before the function returns. - Could
s
be modified whilestr
is still using it? No,str
dies at the end of the function, and the caller has no opportunity to modify thes
before the function returns. - Does
str
modify the string in some way that the caller would not expect? No, the function does not modify the string at all.
Since all three of these criteria are false, there is no risk in using the string that s
is holding instead of making a copy. And since string copies are expensive, why pay for one that we don’t need?
std::string_view
is a viewer
std::string_view
takes a different approach to initialization. Instead of making an expensive copy of the initialization string, std::string_view
creates an inexpensive view of the initialization string. The std::string_view
can then be used whenever access to the string is required.
In the context of our analogy, std::string_view
is a viewer. It views an object that already exists elsewhere, and cannot modify that object. When the view is destroyed, the object being viewed is not affected.
It is important to note that a std::string_view
remains dependent on the initializer through its lifetime. If the string being viewed is modified or destroyed while the view is still being used, unexpected or undefined behavior will result.
Whenever we use a view, it is up to us to ensure these possibilities do not occur.
Warning
A view is dependent on the object being viewed. If the object being viewed is modified or destroyed while the view is still being used, unexpected or undefined behavior will result.
A std::string_view
that is viewing a string that has been destroyed is sometimes called a dangling view.
std::string_view
is best used as a read-only function parameter
The best use for std::string_view
is as a read-only function parameter. This allows us to pass in a C-style string, std::string
, or std::string_view
argument without making a copy, as the std::string_view
will create a view to the argument.
#include <iostream>
#include <string>
#include <string_view>
void printSV(std::string_view str) // now a std::string_view, creates a view of the argument
{
std::cout << str << '\n';
}
int main()
{
printSV("Hello, world!"); // call with C-style string literal
std::string s2{ "Hello, world!" };
printSV(s2); // call with std::string
std::string_view s3 { s2 };
printSV(s3); // call with std::string_view
return 0;
}
Because the str
function parameter is created, initialized, used, and destroyed before control returns to the caller, there is no risk that the string being viewed (the function argument) will be modified or destroyed before our str
parameter.
Should I prefer std::string_view
or const std::string&
function parameters? Advanced
Prefer std::string_view
in most cases. We cover this topic further in lesson 12.6 -- Pass by const lvalue reference.
Improperly using std::string_view
Let’s take a look at a few cases where misusing std::string_view
gets us into trouble.
Here’s our first example:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string_view sv{};
{ // create a nested block
std::string s{ "Hello, world!" }; // create a std::string local to this nested block
sv = s; // sv is now viewing s
} // s is destroyed here, so sv is now viewing an invalid string
std::cout << sv << '\n'; // undefined behavior
return 0;
}
In this example, we’re creating std::string s
inside a nested block (don’t worry about what a nested block is yet). Then we set sv
to view s
. s
is then destroyed at the end of the nested block. sv
doesn’t know that s
has been destroyed. When we then use sv
, we are accessing an invalid object, and undefined behavior results.
Here’s another variant of the same issue, where we initialize a std::string_view
with the std::string
return value of a function:
#include <iostream>
#include <string>
#include <string_view>
std::string getName()
{
std::string s { "Alex" };
return s;
}
int main()
{
std::string_view name { getName() }; // name initialized with return value of function
std::cout << name << '\n'; // undefined behavior
return 0;
}
This behaves similarly to the prior example. The getName()
function is returning a std::string
containing the string “Alex”. Return values are temporary objects that are destroyed at the end of the full expression containing the function call. We must either use this return value immediately, or copy it to use later.
But std::string_view
doesn’t make copies. Instead, it creates a view to the temporary return value, which is then destroyed. That leaves our std::string_view
dangling (viewing an invalid object), and printing the view results in undefined behavior.
The following is a less-obvious variant of the above:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
using namespace std::string_literals;
std::string_view name { "Alex"s }; // "Alex"s creates a temporary std::string
std::cout << name << '\n'; // undefined behavior
return 0;
}
A std::string
literal (created via the s
literal suffix) creates a temporary std::string
object. So in this case, "Alex"s
creates a temporary std::string
, which we then use as the initializer for name
. At this point, name
is viewing the temporary std::string
. Then the temporary std::string
is destroyed, leaving name
dangling. We get undefined behavior when we then use name
.
Warning
Do not initialize a std::string_view
with a std::string
literal, as this will leave the std::string_view
dangling.
It is okay to initialize a std::string_view
with a C-style string literal or a std::string_view
literal. It’s also okay to initialize a std::string_view
with a C-style string object, a std::string
object, or a std::string_view
object, as long as that string object outlives the view.
We can also get undefined behavior when the underlying string is modified:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string s { "Hello, world!" };
std::string_view sv { s }; // sv is now viewing s
s = "Hello, a!"; // modifies s, which invalidates sv (s is still valid)
std::cout << sv << '\n'; // undefined behavior
return 0;
}
In this example, sv
is again set to view s
. s
is then modified. When a std::string
is modified, any views into that std::string
are likely to be invalidated, meaning those views are now invalid or incorrect. Using an invalidated view will result in undefined behavior.
For advanced readers
- If the
std::string
reallocates memory in order to accommodate the new string data, it will return the memory used for the old string data back to the operating system. Since thestd::string_view
is still viewing the old string data, it is now dangling (pointing to a now-invalid object). - If the
std::string
does not reallocate memory, it will copy the new string data over the old string data (starting at the same memory address). Thestd::string_view
will now be viewing the new string data (since it was placed at the same memory address as it was viewing), but it will not realize that the length of thestd::string
has probably changed. If the new string is longer than the old string, thestd::string_view
will now be viewing a substring of the new string (of the same length as the old string). If the new string is shorter than the old string, thestd::string_view
will now be viewing a superstring of the new string (consisting of the entire new string, plus whatever garbage characters are still in the memory beyond the end of the string).
Key insight
Modifying a std::string
is likely to invalidate all views into that std::string
.
Revalidating an invalid std::string_view
Invalidated objects can often be revalidated (made valid again) by setting them back to a known good state. For an invalidated std::string_view
, we can do this by assigning the invalidated std::string_view
object a valid string to view.
Here’s the same example as prior, but we’ll revalidate sv
:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string s { "Hello, world!" };
std::string_view sv { s }; // sv is now viewing s
s = "Hello, universe!"; // modifies s, which invalidates sv (s is still valid)
std::cout << sv << '\n'; // undefined behavior
sv = s; // revalidate sv: sv is now viewing s again
std::cout << sv << '\n'; // prints "Hello, universe!"
return 0;
}
After sv
is invalidated by the modification of s
, we revalidate sv
via the statement sv = s
, which causes sv
to become a valid view of s
again. When we print sv
the second time, it prints “Hello, universe!”.
Be careful returning a std::string_view
std::string_view
can be used as the return value of a function. However, this is often dangerous.
Because local variables are destroyed at the end of the function, returning a std::string_view
that is viewing a local variable will result in the returned std::string_view
being invalid, and further use of that std::string_view
will result in undefined behavior. For example:
#include <iostream>
#include <string>
#include <string_view>
std::string_view getBoolName(bool b)
{
std::string t { "true" }; // local variable
std::string f { "false" }; // local variable
if (b)
return t; // return a std::string_view viewing t
return f; // return a std::string_view viewing f
} // t and f are destroyed at the end of the function
int main()
{
std::cout << getBoolName(true) << ' ' << getBoolName(false) << '\n'; // undefined behavior
return 0;
}
In the above example, when getBoolName(true)
is called, the function returns a std::string_view
that is viewing t
. However, t
is destroyed at the end of the function. This means the returned std::string_view
is viewing an object that has been destroyed. So when the returned std::string_view
is printed, undefined behavior results.
Your compiler may or may not warn you about such cases.
There are two main cases where a std::string_view
can be returned safely. First, because C-style string literals exist for the entire program, it’s fine (and useful) to return C-style string literals from a function that has a return type of std::string_view
.
#include <iostream>
#include <string_view>
std::string_view getBoolName(bool b)
{
if (b)
return "true"; // return a std::string_view viewing "true"
return "false"; // return a std::string_view viewing "false"
} // "true" and "false" are not destroyed at the end of the function
int main()
{
std::cout << getBoolName(true) << ' ' << getBoolName(false) << '\n'; // ok
return 0;
}
This prints:
true false
When getBoolName(true)
is called, the function will return a std::string_view
viewing the C-style string "true"
. Because "true"
exists for the entire program, there’s no problem when we use the returned std::string_view
to print "true"
within main()
.
Second, it is generally okay to return a function parameter of type std::string_view
:
#include <iostream>
#include <string>
#include <string_view>
std::string_view firstAlphabetical(std::string_view s1, std::string_view s2)
{
if (s1 < s2)
return s1;
return s2;
}
int main()
{
std::string a { "World" };
std::string b { "Hello" };
std::cout << firstAlphabetical(a, b) << '\n'; // prints "Hello"
return 0;
}
It may be less obvious why this is okay. First, note that arguments a
and b
exist in the scope of the caller. When the function is called, function parameter s1
is a view into a
, and function parameter s2
is a view into b
. When the function returns either s1
or s2
, it is returning a view into a
or b
back to the caller. Since a
and b
still exist at this point, it’s fine to use the returned std::string_view
into a
or b
.
There is one important subtlety here. If the argument is a temporary object (that will be destroyed at the end of the full expression containing the function call), the std::string_view
return value must be used in the same expression. After that point, the temporary is destroyed and the std::string_view is left dangling.
Warning
If an argument is a temporary that is destroyed at the end of the full expression containing the function call, the returned std::string_view
must be used immediately, as it will be left dangling after the temporary is destroyed.
View modification functions
Consider a window in your house, looking at an electric car sitting on the street. You can look through the window and see the car, but you can’t touch or move the car. Your window just provides a view to the car, which is a completely separate object.
Many windows have curtains, which allow us to modify our view. We can close either the left or right curtain to reduce what we can see. We don’t change what’s outside, we just reduce the visible area.
Because std::string_view
is a view, it contains functions that let us modify our view by “closing the curtains”. This does not modify the string being viewed in any way, just the view itself.
- The
remove_prefix()
member function removes characters from the left side of the view. - The
remove_suffix()
member function removes characters from the right side of the view.
#include <iostream>
#include <string_view>
int main()
{
std::string_view str{ "Peach" };
std::cout << str << '\n';
// Remove 1 character from the left side of the view
str.remove_prefix(1);
std::cout << str << '\n';
// Remove 2 characters from the right side of the view
str.remove_suffix(2);
std::cout << str << '\n';
str = "Peach"; // reset the view
std::cout << str << '\n';
return 0;
}
This program produces the following output:
Peach each ea Peach
Unlike real curtains, once remove_prefix()
and remove_suffix()
have been called, the only way to reset the view is by reassigning the source string to it again.
std::string_view
can view a substring
This brings up an important use of std::string_view
. While std::string_view
can be used to view an entire string without making a copy, they are also useful when we want to view a substring without making a copy. A substring is a contiguous sequence of characters within an existing string. For example, given the string “snowball”, some substrings are “snow”, “all”, and “now”. “owl” is not a substring of “snowball” because these characters do not appear contiguously in “snowball”.
std::string_view
may or may not be null-terminated
The ability to view just a substring of a larger string comes with one consequence of note: a std::string_view
may or may not be null-terminated.
Related content
We cover what a null-terminated string is in lesson 5.2 -- Literals.
Consider the string “snowball”, which is null-terminated (because it is a C-style string literal, which are always null-terminated). If a std::string_view
views the whole string, then it is viewing a null-terminated string. However, if std::string_view
is only viewing the substring “now”, then that substring is not null-terminated (the next character is a ‘b’).
Key insight
A C-style string literal and a std::string
are always null-terminated.
A std::string_view
may or may not be null-terminated.
In almost all cases, this doesn’t matter -- a std::string_view
keeps track of the length of the string or substring it is viewing, so it doesn’t need the null-terminator. Converting a std::string_view
to a std::string
will work regardless of whether or not the std::string_view
is null-terminated.
Warning
Take care not to write any code that assumes a std::string_view
is null terminated.
Tip
If you have a non-null-terminated std::string_view
and you need a null-terminated string for some reason, convert the std::string_view
into a std::string
.
A quick guide on when to use std::string
vs std::string_view
This guide is not meant to be comprehensive, but is intended to highlight the most common cases:
Variables
Use a std::string
variable when:
- You need a string that you can modify.
- You need to store user-inputted text.
- You need to store the return value of a function that returns a
std::string
.
Use a std::string_view
variable when:
- You need read-only access to part or all of a string that already exists elsewhere and will not be modified or destroyed before use of the
std::string_view
is complete. - You need a symbolic constant for a C-style string.
- You need to continue viewing the return value of a function that returns a C-style string or a non-dangling
std::string_view
.
Function parameters
Use a std::string
function parameter when:
- The function needs to modify the string passed in as an argument without affecting the caller. This is rare.
- You are using language standard C++14 or older and aren’t comfortable using references yet.
Use a std::string_view
function parameter when:
- The function needs a read-only string.
- The function needs to work with non-null-terminated strings.
For advanced readers
Also see 12.6 -- Pass by const lvalue reference.
Use a const std::string&
function parameter when:
- You are using language standard C++14 or older, and the function needs a read-only string to work with (as
std::string_view
is not available until C++17). - You are calling other functions that require a
const std::string
,const std::string&
, or const C-style string (asstd::string_view
may not be null-terminated).
Use a std::string&
function parameter when:
- You are using a
std::string
as an out-parameter (see 12.13 -- In and out parameters). - You are calling other functions that require a
std::string&
, or non-const C-style string.
Return types
Use a std::string
return type when:
- The return value is a
std::string
local variable or function parameter. - The return value is a function call or operator that returns a
std::string
by value.
Use a std::string_view
return type when:
- The function returns a C-style string literal or local
std::string_view
that has been initialized with a C-style string literal. - The function returns a
std::string_view
parameter.
For advanced readers
See lesson 12.12 -- Return by reference and return by address for more information on returning reference types.
Use a std::string_view
return type when:
- Writing an accessor for a
std::string_view
member.
Use a std::string&
return type when:
- The function returns a
std::string&
parameter.
Use a const std::string&
return type when:
- The function returns a
const std::string&
parameter. - Writing an accessor for a
std::string
orconst std::string
member. - The function returns a static (local or global)
const std::string
.
Insights
Things to remember about std::string
:
- Initializing and copying
std::string
is expensive, so avoid this as much as possible. - Avoid passing
std::string
by value, as this makes a copy. - If possible, avoid creating short-lived
std::string
objects. - Modifying a
std::string
will invalidate any views to that string. - It is okay to return a local
std::string
by value.
Things to remember about std::string_view
:
std::string_view
is typically used for passing string function parameters and returning string literals.- Because C-style string literals exist for the entire program, it is always okay to set a
std::string_view
to a C-style string literal. - When a string is destroyed, all views to that string are invalidated.
- Using an invalidated view (other than using assignment to revalidate the view) will cause undefined behavior.
- A
std::string_view
may or may not be null-terminated.