Standard Library[lib]
C++ string find: how to find a substring and check npos
- Section
- Standard Library [lib]
- Standard
- C++17
- Compilers
- MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2, Clang 23.1 (libc++)
- Published
- Length
- 7 min read
In this answer8 clauses
- 1What does find return, and what is std::string::npos?
- 2How do you check if a string contains a substring?
- 3Why does if (text.find(x)) give the wrong answer?
- 4How do you find every occurrence of a substring?
- 5find, rfind, find_first_of: what is the difference?
- 6How do you find a substring ignoring case?
- 7std::string::find or std::find?
- 8A C++ string search cheat sheet
Abstract
To find a substring in a C++ string, call text.find(x): it returns the index where the first match starts, counting from 0, or std::string::npos, the largest std::size_t, when there is none. Test it with text.find(x) != std::string::npos, not with if (text.find(x)), which takes a match at index 0 for a miss and a miss for a match. Since C++23, text.contains(x) answers yes or no in one call.

C++ string find is find(), a member function of std::string: text.find("needle") returns the index of the first match, counting from 0, or the constant std::string::npos when there is no match. So the test is if (text.find("needle") != std::. An index of 0 is a match at the very start, not “not found”.
Finding the @ and the domain in an email address, as a whole program:
#include <cstddef>
#include <iostream>
#include <string>
int main() {
std::string email = "ana@example.com";
std::cout << email.find('@') << '\n'; // 3
std::cout << email.find("example") << '\n'; // 4
std::size_t pos = email.find("gmail");
if (pos != std::string::npos) {
std::cout << "found at " << pos << '\n';
} else {
std::cout << "not found\n"; // printed
}
}— end example]
It printed:
3
4
not found— end example]
The @ is at index 3 because counting starts at 0, and “example” starts right after it. The samples are C++17, built with MSVC 19.44, Clang 19.1 (MSVC STL), GCC 16.2 and Clang 23.1 (libc++), and all four printed the same for every sample. Since When lists the first compiler releases for each standard’s features.
What does find return, and what is std::string::npos?
find() returns a std::size_t, an unsigned number (one that cannot be negative): the index where the match starts. std::string::npos is the largest value a std::size_t can hold, set aside to mean “no position”:
static_assert(std::string::npos ==
std::numeric_limits<std::size_t>::max());
std::cout << std::string::npos << '\n';— end example]
The check compiled on all four, and each printed 18446744073709551615, the largest std::size_t on a 64-bit build. The standard declares npos as size_type(-1), which is that same largest value. So keep the result in a std::size_t or an auto: after auto pos = email.find('#');, pos is a std::size_t, and pos == std:: printed true. find() works the same way on a std::string_view.

How do you check if a string contains a substring?
Compare find() with npos: text.find(x) != std:: is true when x occurs anywhere in text. Since C++23, text.contains(x) asks the same question in one call, and since C++20, starts_with() and ends_with() test the two ends:
std::cout << std::boolalpha;
std::cout << email.contains("@example") << '\n'; // C++23: true
std::cout << email.contains('#') << '\n'; // C++23: false
std::cout << email.starts_with("ana") << '\n'; // C++20: true
std::cout << email.ends_with(".com") << '\n'; // C++20: true
// the same questions in C++17
std::cout << (email.find("@example") != std::string::npos) << '\n';
std::cout << (email.find('#') != std::string::npos) << '\n';
std::cout << (email.rfind("ana", 0) == 0) << '\n'; // starts with— end example]
Before C++20, rfind("ana", 0) == 0 is the starts-with test: searching backwards from index 0, it can only match at the start.
Why does if (text.find(x)) give the wrong answer?
Because find() returns an index, not a yes or no. In an if, a match at index 0 converts to false and npos converts to true, so the test is wrong in exactly those two cases, and right only for a match after index 0:
if (email.find("ana")) { // 0 converts to false
std::cout << "found\n";
} else {
std::cout << "not found\n"; // printed, but "ana" is there
}
if (email.find("zzz")) { // npos converts to true
std::cout << "found\n"; // printed, but "zzz" is not
} else {
std::cout << "not found\n";
}— end example]
All four printed “not found” for “ana”, which is there, and “found” for “zzz”, which is not. None of the four warned: not MSVC at /W4, not GCC or Clang with -Wall -Wextra.
Storing the result in an int, or comparing it with -1, also gave the right answer, through a conversion the line does not show, and it reads as if find() returned -1: a failed search stored in an int printed -1 on all four, and email.find("zzz") == -1 compiled and was true. Only MSVC warned about the int (a possible loss of data), and only GCC and Clang about the comparison (signed against unsigned), so at those warning levels no compiler caught both.
How do you find every occurrence of a substring?
Give find() a start position: its second argument is the index where the search begins, and a match may not start before it ([string.view.find]). Loop until it returns npos, starting each search one past the last match to count overlapping matches, or the needle’s length past it to skip them:
std::vector<std::size_t> find_all(const std::string& text,
const std::string& needle,
bool overlapping = true) {
std::vector<std::size_t> found;
if (needle.empty()) {
return found; // "" matches at every index: stop here
}
std::size_t step = overlapping ? 1 : needle.size();
std::size_t pos = text.find(needle); // the first match
while (pos != std::string::npos) {
found.push_back(pos);
pos = text.find(needle, pos + step); // the next one after it
}
return found;
}
// ...
print(find_all("the cat sat on the mat", "at")); // 5 9 20
print(find_all("aaaa", "aa")); // 0 1 2
print(find_all("aaaa", "aa", false)); // 0 2
print(find_all("aaaa", "")); // none— end example]
All four printed 5 9 20, then 0 1 2, 0 2 and none (print writes the indexes on one line). The first search starts at 0, so a match at index 0 is kept. The early return is there because an empty needle matches at every position, and a step of its length, 0, would never move pos: the loop would not end.
find, rfind, find_first_of: what is the difference?
find() and rfind() look for the whole search text, find() the first match and rfind() the last; find_first_of() and find_last_of() look for any one character from a set, find_first_not_of() for the first character outside it. On the path “C:/data/report.final.csv”, all four printed:
| Call | Finds | Result |
|---|---|---|
find('.') |
the first dot | 14 |
rfind('.') |
the last dot | 20 |
find_first_of("./") |
the first dot or slash | 2 |
find_last_of("/") |
the last slash | 7 |
find_first_not_of("C:/") |
the first character that is not C, a colon or a slash | 3 |
The mistake is to read find_first_of() as a search for a word:
std::string path = "C:/data/report.final.csv";
// ...
std::cout << path.find_first_of("final") << '\n'; // 4
std::cout << path.find("final") << '\n'; // 15— end example]
It returned 4, the a of data, because it stops at the first character that is any of f, i, n, a and l; find("final") returned 15, where the word starts. To cut the string at the index you found, substr() takes a start and a length, and splitting a string puts the two in a loop.
How do you find a substring ignoring case?
Write a small function, because find() compares characters exactly and has no option to ignore case: on “Ana@Example.COM”, email.find("example") printed “not found”. std::search, from <algorithm>, takes the comparison as its last argument; give it a lambda (a function written in place) that lowercases both characters:
// ASCII letters only: std::tolower in the default "C" locale
std::size_t find_ignore_case(const std::string& text,
const std::string& needle) {
auto same = [](char a, char b) {
return std::tolower(static_cast<unsigned char>(a)) ==
std::tolower(static_cast<unsigned char>(b));
};
auto it = std::search(text.begin(), text.end(),
needle.begin(), needle.end(), same);
if (it == text.end()) {
return std::string::npos;
}
return static_cast<std::size_t>(it - text.begin());
}
// ...
std::string email = "Ana@Example.COM";
report(email.find("example")); // not found
report(find_ignore_case(email, "example")); // found at 4
report(find_ignore_case(email, "gmail")); // not found— end example]
All four printed “not found”, “found at 4” and “not found”. std::search returns an iterator, a position in the string, at the start of the first match, or text.end() when there is none; subtracting text.begin() turns it into an index. The function handles the letters A to Z only, because in the default “C” locale std::tolower changes no others.
std::string::find or std::find?
For a substring, the member function. email.find('@') searches the string’s text and returns an index; the algorithm std::find, from <algorithm>, looks for one element in any range and returns an iterator. auto it = std:: and then it - email.begin() printed 3, the same as email.find('@'), but it looks for one character, never a substring. std::find on a vector, array or string covers the algorithm.
A C++ string search cheat sheet
| You want | Write |
|---|---|
| Is it there? (C++23) | s.contains(x) |
| Is it there? (C++17) | s.find(x) != std:: |
| Where is the first? | s.find(x) |
| Where is the last? | s.rfind(x) |
| The next one after pos | s.find(x, pos + 1) |
| Does it start with x? (C++20) | s.starts_with(x) |
| Does it start with x? (C++17) | s.rfind(x, 0) == 0 |
| Does it end with x? (C++20) | s.ends_with(x) |
| Any of these characters | s.find_first_of("chars") |
| Ignoring case | std::search with a lowercasing comparison |
More answers on strings and the rest of the library are in the Standard library section.
Questions
What is the value of std::string::npos?
It is the largest value a std::size_t can hold: 18446744073709551615 on a 64-bit build, the number MSVC, GCC and Clang all printed. It is not -1, although the standard declares it as size_type(-1), which is that same largest value. Compare the result of find() with std::string::npos, not with -1.
What does find return if the string is not found?
It returns std::string::npos, a constant of type std::size_t. It does not return -1 or 0 for a miss: 0 means the match starts at the first character. Test it with text.find(x) != std::string::npos, or, since C++23, ask text.contains(x).
Is std::string::find case sensitive?
Yes. find() compares characters exactly, so on “Ana@Example.COM” a search for “example” finds nothing and returns std::string::npos. To ignore case, use std::search with a comparison that lowercases both characters with std::tolower, which in the default locale changes only the letters A to Z.
What does find return when you search for an empty string?
An empty search text is found where the search starts, index 0 unless you pass a start position, as long as that position is not past the end. On the 15-character “ana@example.com”, find(“”) returned 0, find(“”, 15) returned 15, and find(“”, 99) returned std::string::npos.
How do you find a character in a string in C++?
Call the same find() with a char: email.find(‘@’) returns the index of the first @, or std::string::npos when there is none. rfind(‘@’) looks for the last one, and find_first_of(“@.”) for the first character that is any of several.
How do you find the last occurrence of a substring?
Call rfind(), which searches from the end of the string toward the start and returns the index where the last match starts, counted from the beginning. On “C:/data/report.final.csv”, rfind(‘.’) returned 20, the dot before the extension, where find(‘.’) returned 14.
Sources
- std::basic_string::findcppreference.com
- std::basic_string::nposcppreference.com
- std::basic_string::rfindcppreference.com
- std::basic_string::find_first_ofcppreference.com
- std::basic_string::containscppreference.com
- std::basic_string::starts_withcppreference.com
- [string.find] basic_string::findC++ standard working draft
- [string.view.find] SearchingC++ standard working draft