Working with String Views
An in-depth guide to std::string_view
, including their methods, operators, and how to use them with standard library algorithms
In 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:
String Views
A practical introduction to string views, and why they should be the main way we pass strings to functions
The size()
Method
We 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.
Accessing Individual String View Characters
Similar to arrays and other contiguous containers, string views give access to random characters within their purview.
The []
operator
Random 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.
The at()
method
We 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:
Exceptions: throw
, try
and catch
This lesson provides an introduction to exceptions, detailing the use of throw
, try
, and catch
.
The front()
method
We 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
The back()
method
The 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
Comparing String Views
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
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
Analysing String View Contents
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:
The contains()
method
The 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
The starts_with()
method
The 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"
The ends_with()
method
Finally, 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
Searching String Views
We can search our string views for specific substrings using a range of methods
The find()
method
The 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
The rfind()
method
The 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
The find_first_of()
method
The 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
The find_first_not_of()
method
The 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
The find_last_of()
method
The 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
The find_last_not_of()
method
The 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 and Ranges
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,
Iterator and Range-Based Algorithms
An introduction to iterator and range-based algorithms, using examples from the standard library
String Views with Standard Library Algorithms
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
String Views with Standard Library Views
We can also use our string views in conjunction with other views. We covered standard library views earlier in the course:
Standard Library Views
Learn how to create and use views in C++ using examples from std::views
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 View Iterators and Subranges
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
Shrinking a String View In Place
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
Copying Characters from a String View
We can copy the contents of our string view to another character array, using a raw pointer. The copy()
method accepts three arguments:
- A raw pointer to the character array
- The number of characters we want to copy
- The position of the first character we want to copy from. This is an optional parameter defaulted to
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!
Using Regular Expressions with String Views
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:
Regular Expressions
An introduction to regular expressions, and how to use them in C++ with std::regex
, std::regex_match
, and std::regex_search
Regex Capture Groups
An introduction to regular expression capture groups, and how to use them in C++ with regex_search
, regex_replace
, regex_iterator
, and regex_token_iterator
Summary
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.
Main Points Learned
- The functionality and use cases of
std::string_view
methods such assize()
,remove_suffix()
, andremove_prefix()
. - How to access individual characters in a string view using the
[]
operator and theat()
method, including bounds checking withat()
. - The process of comparing string views using comparison operators and the importance of efficient string comparison techniques.
- Various methods for analyzing string view contents, including
contains()
,starts_with()
, andends_with()
. - Techniques for searching within string views using
find()
,rfind()
,find_first_of()
, and related methods. - The compatibility of string views with standard library algorithms and views demonstrates their use in range-based for loops and with algorithms like
std::ranges::count()
. - How to work with string views and regular expressions for pattern matching, utilizing iterator-based methods for compatibility.
- Practical examples of modifying string views in place using
remove_prefix()
andremove_suffix()
, and the implications of viewing versus owning string data. - Copying characters from a string view to another character array using the
copy()
method.
Output Streams
A detailed overview of C++ Output Streams, from basics and key functions to error handling and custom types.