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
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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:

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:

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

String Comparisons are Usually a Red Flag

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

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,

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:

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:

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 as size(), remove_suffix(), and remove_prefix().
  • How to access individual characters in a string view using the [] operator and the at() method, including bounds checking with at().
  • 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(), and ends_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() and remove_suffix(), and the implications of viewing versus owning string data.
  • Copying characters from a string view to another character array using the copy() method.

Was this lesson useful?

Next Lesson

Output Streams

A detailed overview of C++ Output Streams, from basics and key functions to error handling and custom types.
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Strings and Streams
Next Lesson

Output Streams

A detailed overview of C++ Output Streams, from basics and key functions to error handling and custom types.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved