std::string_view
, including their methods, operators, and how to use them with standard library algorithmsIn this lesson, we expand our knowledge of std::string_view
by exploring its methods and operators. This includes working with the individual characters of a string view, analyzing its contents, shrinking its purview, and more.
We also show some practical examples of how string views can interact with other parts of the standard library.
This includes standard library views, algorithms, regular expressions, and anything that works with iterators.
This builds upon our previous introduction to string views, so familiarity with the content covered there is assumed:
size()
MethodWe can retrieve the number of characters in our string view using the size()
 method:
#include <iostream>
int main(){
std::string_view View{"Hello World"};
std::cout << "View size: " << View.size();
}
View size: 11
Note that the size of the string view is not necessarily the size of the underlying string. The previous lesson showed how we could create a view that only contains part of the string, using the iterator constructor:
#include <iostream>
int main(){
std::string_view String{"Hello World"};
std::string_view View{
String.begin(), String.begin() + 5};
std::cout << "String size: " << String.size();
std::cout << "\nView size: " << View.size();
}
String size: 11
View size: 5
The remove_suffix()
and remove_prefix()
methods, which we will cover later in this lesson, also reduce the size of the string view.
Similar to arrays and other contiguous containers, string views give access to random characters within their purview.
[]
operatorRandom character access is typically performed using the []
 operator:
#include <iostream>
int main(){
std::string_view View{"Hello World"};
std::cout << "First: " << View[0]
<< "\nSecond: " << View[1]
<< "\nLast: " << View[View.size() - 1];
}
First: H
Second: e
Last: d
In most implementations, we get bounds checking when using []
if our application is compiled with debug flags. Those checks are then stripped out for release builds, to optimize performance.
at()
methodWe can alternatively use at()
, which performs run-time bounds checking on our index even in release builds. This has a small performance cost, but throws a std::out_of_range
exception if our index is out of bounds:
#include <iostream>
int main(){
std::string_view View{"Hello World"};
std::cout << "First: " << View.at(0)
<< "\nSecond: " << View.at(1);
try {
View.at(100);
} catch (const std::out_of_range& e) {
std::cout << "\nSomething went wrong:\n";
std::cout << e.what();
}
}
First: H
Second: e
Something went wrong:
invalid string_view position
We covered exceptions in a dedicated chapter earlier in the course:
front()
methodWe can access the first character in a string view using the front()
method. This is equivalent to View[0]
:
#include <iostream>
int main(){
std::string_view View{"Hello World"};
std::cout << "Front: " << View.front();
}
Front: H
back()
methodThe last character in the string view is available using the back()
method. This is equivalent to View[View.size() - 1]
:
#include <iostream>
int main(){
std::string_view View{"Hello World"};
std::cout << "Back: " << View.back();
}
Back: d
String views implement the full suite of comparison operators:
#include <iostream>
#include <string_view>
int main(){
std::string_view FruitA{"Apple"};
std::string_view FruitB{"Apple"};
if (FruitA == FruitB) {
std::cout << "String views are equal";
}
}
String views are equal
In general, performing an equality operation on strings should be quite an uncommon operation. Comparing strings tends to be a slow operation. Establishing that two strings are equal requires individual comparison operations for every character in the string.
One of the main motivations beginners have for implementing string comparisons is when they have a value that can be one of many different possibilities.
#include <iostream>
#include <string_view>
class Character {
public:
std::string_view Faction;
};
int main(){
Character A{"Human"};
Character B{"Human"};
if (A.Faction == B.Faction) {
std::cout << "They're the same faction";
}
}
They're the same faction
However, an enum is the preferred way of implementing this:
#include <iostream>
enum class Faction { Human, Elf, Undead };
class Character {
public:
Faction Faction;
};
int main(){
Character A{Faction::Human};
Character B{Faction::Human};
if (A.Faction == B.Faction) {
std::cout << "They're the same faction";
}
}
They're the same faction
Enum comparisons require a single operation, and they offer additional benefits too. An enum has a known list of possible values, which means the compiler can protect us against spelling errors, and our IDE can provide us with autocomplete options.
Other comparisons, such as <
and >=
compare string views lexicographically (ie, based on alphabetical order)
#include <iostream>
#include <string_view>
int main(){
std::string_view Apple{"Apple"};
std::string_view Zebra{"Zebra"};
if (Apple < Zebra) {
std::cout << "Apple < Zebra";
}
if (Zebra >= Apple) {
std::cout << "\nZebra >= Apple";
}
}
Apple < Zebra
Zebra >= Apple
Because of this, collections of string views are directly compatible with algorithms that require the objects they’re acting upon to be orderable. Below, we sort a std::vector
of string views into alphabetical order:
#include <iostream>
#include <string_view>
#include <vector>
#include <algorithm>
int main(){
std::vector<std::string_view> Fruits{
"Kiwi", "Apple", "Banana"};
std::ranges::sort(Fruits);
for (const auto& Fruit : Fruits) {
std::cout << Fruit << ", ";
}
}
Apple, Banana, Kiwi
String views are compatible with regular expressions and standard library algorithms, which give us a lot of flexibility in analyzing their contents. We’ll cover those later in this lesson, but some of the most common requirements are available as simple built-in methods:
contains()
methodThe contains()
method returns a boolean representing whether or not the string view contains the specific substring passed as an argument.
#include <iostream>
int main(){
std::string_view Input{"Hello World"};
if (Input.contains("Hello")) {
std::cout << "Greeting Found";
}
}
Greeting Found
starts_with()
methodThe starts_with()
method returns a boolean representing whether or not the string view starts with the string provided as an argument.
#include <iostream>
int main(){
std::string_view Input{"Hello World"};
if (Input.starts_with("Hello")) {
std::cout << "Input starts with \"Hello\"";
}
if (!Input.starts_with("World")) {
std::cout << "\nBut not \"World\"";
}
}
Input starts with "Hello"
But not "World"
ends_with()
methodFinally, the ends_with()
method returns true
if our string view ends with the provided argument.
#include <iostream>
int main(){
std::string_view Input{"Hello World"};
if (Input.ends_with("World")) {
std::cout << "Input ends with \"World\"";
}
if (Input.contains("Hello")) {
std::cout << "\nInput contains \"Hello\"";
}
if (!Input.ends_with("Hello")) {
std::cout << " but it's not at the end";
}
}
Input ends with "World"
Input contains "Hello" but it's not at the end
We can search our string views for specific substrings using a range of methods
find()
methodThe find()
method searches through our string view to find a specific substring. It will then return the index of that substring within our string view:
#include <iostream>
int main(){
std::string_view Input{"Hello world"};
size_t Position{Input.find("world")};
std::cout << "Position: " << Position;
}
Position: 6
If the substring appears multiple times in our string view, find()
will return the index of the first occurrence.
If the substring was not found, find()
returns a token equal to std::string::npos
:
#include <iostream>
int main(){
std::string_view Input{"Hello world"};
size_t Position{Input.find("goodbye")};
if (Position == std::string::npos) {
std::cout << "That wasn't found";
} else {
std::cout << "Position: " << Position;
}
}
That wasn't found
We can pass a second argument to find()
, which allows us to customize at what position our search starts. Below, we use this to find both the first and second occurrences of a substring:
#include <iostream>
int main(){
std::string_view Input{
"Hello world, goodbye world"};
size_t First{Input.find("world")};
size_t Second{Input.find("world", First + 1)};
std::cout << "First: " << First
<< "\nSecond: " << Second;
}
First: 6
Second: 21
rfind()
methodThe rfind()
method searches the string view in reverse order, returning the position of the last occurrence of the substring:
#include <iostream>
int main(){
std::string_view Input{
"Hello world, goodbye world"};
std::cout
<< "First: " << Input.find("world")
<< "\nLast: " << Input.rfind("world");
}
First: 6
Last: 21
Similar to find()
, rfind()
will return an object equal to std::string::npos
if the substring wasn’t found.
We can also pass an additional argument to the function, representing where we want our search to start. Remember, rfind()
searches in reverse order, so our search will proceed backward from this position:
#include <iostream>
int main(){
std::string_view Input{
"Hello world, goodbye world"};
size_t Last{Input.rfind("world")};
size_t SecondLast{
Input.rfind("world", Last - 1)};
std::cout << "Last: " << Last
<< "\nSecond Last: " << SecondLast;
}
Last: 21
Second Last: 6
find_first_of()
methodThe find_first_of()
method accepts a string of characters and returns the index of the first position in our string view that matches any of those characters:
#include <iostream>
int main(){
std::string_view Input{"Hello world"};
std::cout << "First vowel: "
<< Input.find_first_of("aeiou");
}
First vowel: 1
Similar to find()
and rfind()
, an object equal to std::string::npos
is returned if no matching characters were found:
#include <iostream>
int main(){
std::string_view Input{"Hello world"};
size_t Position{
Input.find_first_of("0123456789")};
if (Position == std::string::npos) {
std::cout << "No numbers found";
}
}
No numbers found
And we can pass an additional argument to change the position where our search starts:
#include <iostream>
int main(){
std::string_view Input{"Hello world"};
size_t First{Input.find_first_of("aeiou")};
size_t Second{
Input.find_first_of("aeiou", First + 1)};
std::cout
<< "First Vowel: " << Input[First]
<< "\nSecond Vowel: " << Input[Second];
}
First Vowel: e
Second Vowel: o
find_first_not_of()
methodThe find_first_not_of()
method behaves in the same way as find_first_of()
, but it will search for the first character that does not match any of the characters in our argument:
#include <iostream>
int main(){
std::string_view Input{"hey"};
size_t First{
Input.find_first_not_of("aeiou")};
size_t Second{
Input.find_first_not_of("aeiou",
First + 1)};
size_t Third{
Input.find_first_not_of("aeiou",
Second + 1)};
std::cout
<< "First consonant: " << Input[First]
<< "\nSecond consonant: " << Input[Second];
if (Third == std::string::npos) {
std::cout << "\nThere are no more";
}
}
First consonant: h
Second consonant: y
There are no more
find_last_of()
methodThe find_last_of()
method has identical behavior to find_first_of()
, with the only difference being that the search runs from the end of our string view to the beginning:
#include <iostream>
int main(){
std::string_view Input{"hello"};
size_t Last{Input.find_last_of("aeiou")};
size_t SecondLast{
Input.find_last_of("aeiou", Last - 1)};
size_t ThirdLast{
Input.find_last_of("aeiou",
SecondLast - 1)};
std::cout
<< "Last vowel: " << Input[Last]
<< "\nSecond last vowel: "
<< Input[SecondLast];
if (ThirdLast == std::string::npos) {
std::cout << "\nThere are no more";
}
}
Last vowel: o
Second last vowel: e
There are no more
find_last_not_of()
methodThe find_last_not_of()
function behaves similarly to find_first_not_of()
. The only exception is that the search starts at the end of our string view and proceeds backward:
#include <iostream>
int main(){
std::string_view Input{"hey"};
size_t Last{Input.find_last_not_of("aeiou")};
size_t SecondLast{
Input.find_last_not_of("aeiou", Last - 1)};
size_t ThirdLast{
Input.find_last_not_of("aeiou",
SecondLast - 1)};
std::cout
<< "Last consonant: " << Input[Last]
<< "\nSecond last consonant: "
<< Input[SecondLast];
if (ThirdLast == std::string::npos) {
std::cout << "\nThere are no more";
}
}
Last consonant: y
Second last consonant: h
String views are ranges, so are compatible with a lot of other language features and standard library utilities. For example, we can use a string view in a range-based for loop, iterating over every character in the string view:
#include <iostream>
int main(){
std::string_view Name{"Anna"};
for (char C : Name) {
std::cout << C << ", ";
}
}
A, n, n, a,
String views are also compatible with the standard library algorithms we covered earlier in the course. In this example, we use std::ranges::count()
to count the number of occurrences of the n
character in our string:
#include <iostream>
#include <algorithm>
int main(){
std::string_view Fruit{"Banana"};
std::cout << "Number of 'n's: " <<
std::ranges::count(Fruit, 'n');
}
Number of 'n's: 2
We can also use our string views in conjunction with other views. We covered standard library views earlier in the course:
In this example, we use std::ranges::take()
to create a view of only the first 3Â characters:
#include <iostream>
#include <ranges>
int main(){
std::string_view Name{"Anna"};
for (char C : std::views::take(Name, 3)) {
std::cout << C << ", ";
}
}
A, n, n,
Below, we have a more complex example that uses std::views::zip()
and std::views::iota()
to generate a more complex output:
#include <iostream>
#include <ranges>
int main(){
using std::views::iota, std::views::zip;
std::string_view Name{"Anna"};
for (auto T : zip(iota(1), Name)) {
std::cout << "Character "
<< std::get<0>(T) << ": "
<< std::get<1>(T) << '\n';
}
}
Character 1: A
Character 2: n
Character 3: n
Character 4: a
String views support the usual collection of iterators, making them widely interoperable with other aspects of the standard library. Below, we use these iterators to create a std::ranges::subrange()
that views part of the string view:
#include <algorithm>
#include <iostream>
int main(){
std::string_view Name{"Anna"};
std::ranges::subrange Subrange{
Name.begin(), Name.begin() + 3};
for (char C : Subrange) {
std::cout << C << ", ";
}
}
A, n, n,
Rather than creating a subrange from a string view, we also have the option to create another string view.
Using the iterator constructor, we can constrict this second string view to contain only a subset of the original characters:
#include <iostream>
int main(){
std::string_view Name{"Anna"};
std::string_view Subview{
Name.begin(), Name.begin() + 3};
for (char C : Subview) {
std::cout << C << ", ";
}
}
A, n, n,
This is similar to the previous example, but our new object is now a std::string_view
rather than a std::ranges::subrange
We can constrain a string view in place, using the remove_prefix()
and remove_suffix()
methods. These remove characters from the start of the view and the end of the view respectively. We pass an integer argument, representing how many characters we want to remove:
#include <iostream>
int main(){
std::string_view View{"--Hello-"};
std::cout << "View: " << View;
View.remove_suffix(1);
std::cout << "\nView: " << View;
View.remove_prefix(2);
std::cout << "\nView: " << View;
}
View: --Hello-
View: --Hello
View: Hello
Remember, a string view is simply a view of an underlying string. Reducing the extent of the view does not modify the underlying string - it just changes the part of the string that is being viewed.
#include <iostream>
int main(){
std::string Name{"Anna"};
std::string_view View{Name};
View.remove_prefix(1);
View.remove_suffix(1);
std::cout << "String: " << Name;
std::cout << "\nView: " << View;
}
String: Anna
View: nn
We can copy the contents of our string view to another character array, using a raw pointer. The copy()
method accepts three arguments:
0
We should ensure our pointer points at a memory location with enough space to receive the characters we will be copying to it.
In this example, we assemble a char*
containing the contents "Hello World!"
using the characters from string views:
#include <iostream>
int main(){
std::string_view InputA{"Hello"};
std::string_view InputB{" World"};
std::string_view InputC{"Nice!"};
char Output[13]{"------------"};
std::cout << Output;
// Copy 5 characters from InputA to Output
InputA.copy(Output, 5);
std::cout << '\n' << Output;
// Copy 6 characters from InputB to Output
// Starting at Output[6]
InputB.copy(Output + 5, 6);
std::cout << '\n' << Output;
// Copy 1 character from InputC to Output
// Starting at InputC[4] and Output[12]
InputC.copy(Output + 11, 1, 4);
std::cout << '\n' << Output;
}
------------
Hello-------
Hello World-
Hello World!
The standard library’s regular expression utilities have limited support for string views, but we do have some options. These options typically involve using the constructors and methods that accept iterators.
Below, we check if our string view contains the pattern "hello"
, without respecting capitalization. Effectively, this is an alternative to the contains()
method when we want our search to be case-insensitive:
#include <iostream>
#include <regex>
int main(){
std::string_view Input{"Hello world"};
std::regex Pattern("hello",
std::regex_constants::icase);
bool MatchResult{
std::regex_search(Input.begin(),
Input.end(),
Pattern)};
if (MatchResult) {
std::cout << "Greeting found";
}
}
Greeting found
In this example, we check if our string view contains either "hello"
or "hi"
:
#include <iostream>
#include <regex>
int main(){
std::string_view Input{"hello world"};
bool MatchResult{
std::regex_search(Input.begin(),
Input.end(),
std::regex("hello|hi"))};
if (MatchResult) {
std::cout << "Greeting found";
}
}
Greeting found
We have dedicated lessons on regular expressions and the flexibility they give us here:
In this lesson, we explored std::string_view
, demonstrating how it can be used to view and manipulate strings without owning them. We covered a range of methods and operations that make std::string_view
a powerful tool for working with strings in a performant and flexible manner.
std::string_view
methods such as size()
, remove_suffix()
, and remove_prefix()
.[]
operator and the at()
method, including bounds checking with at()
.contains()
, starts_with()
, and ends_with()
.find()
, rfind()
, find_first_of()
, and related methods.std::ranges::count()
.remove_prefix()
and remove_suffix()
, and the implications of viewing versus owning string data.copy()
method.An in-depth guide to std::string_view
, including their methods, operators, and how to use them with standard library algorithms
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.