Standard Library[lib]
C++ substring: how to use substr(), with examples
- 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 answer7 clauses
- 1Is the second argument of substr() a length or an end position?
- 2How do you get the first or last n characters?
- 3How do you get the substring after a character, or between two?
- 3.1Why does substr(find(x) + 1) return the whole string?
- 4What happens when pos or count is too large?
- 5How do you check if a string contains a substring?
- 6How do you take a substring without copying?
- 7substr() at a glance
Abstract
To get a substring in C++, call s.substr(pos, count) on a std::string: it returns a new string of up to count characters starting at position pos, counting from 0. The second argument is a length, not an end position, so cut from start up to end with substr(start, end - start). Before you cut after find(), check that it did not return std::string::npos.

To get a substring in C++, call substr() on a std::string: s.substr(pos, count) returns a new string of up to count characters starting at position pos, counting from 0. The second argument is a length, not an end position. Leave it out and you get everything from pos to the end.
A date, cut into its year, month and day:
#include <iostream>
#include <string>
int main() {
std::string date = "2026-11-12";
std::string year = date.substr(0, 4); // 4 characters from 0
std::string month = date.substr(5, 2); // 2 characters from 5
std::string day = date.substr(8); // from 8 to the end
std::cout << year << '\n' << month << '\n' << day << '\n';
}— end example]
It printed:
2026
11
12— end example]
The month starts at position 5 because the year and the dash fill positions 0 to 4. 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 unless a sentence says otherwise. The standard defines substr(pos, n) as a new string made from those characters ([string.substr]); the original is left as it was.
Is the second argument of substr() a length or an end position?
A length: date.substr(5, 7) is up to seven characters starting at position 5, not the characters from position 5 to 7. Java, JavaScript and Python usually take an end position there, and the mistake compiles and runs without a word:
std::string date = "2026-11-12";
// 7 is a length: up to 7 characters from position 5
std::cout << '[' << date.substr(5, 7) << "]\n";
// from 5 up to, not including, 7: the length is 7 - 5
std::cout << '[' << date.substr(5, 7 - 5) << "]\n";— end example]
It printed:
[11-12]
[11]— end example]
The first call asked for seven characters and got the five that were left. To cut from start up to, not including, end, pass the difference as the length: substr(start, end - start).

How do you get the first or last n characters?
The first n characters are s.substr(0, n), and the last n are s.substr(s.size() - n), which starts n characters before the end. The first n need no check, because a count that runs past the end is cut short; the last n do, because s.size() - n wraps around to a huge number when n is larger than the size:
std::string last_n(const std::string& text, std::size_t n) {
if (n >= text.size()) {
return text; // too short: all of it
}
return text.substr(text.size() - n);
}
// ...
std::cout << date.substr(0, 4) << '\n'; // first 4
std::cout << date.substr(date.size() - 2) << '\n'; // last 2
std::cout << last_n(date, 2) << '\n';
std::cout << last_n(date, 20) << '\n'; // more than it has— end example]
It printed 2026, 12, 12, and then the whole date for last_n(date, 20). size() returns an unsigned number, one that has no negative values: without the check, date.size() - 20 wrapped around to 18446744073709551606 on a 64-bit build, and substr() threw std::out_of_range.
How do you get the substring after a character, or between two?
Find the character with find(), or with rfind() for its last occurrence, check the result against std::string::npos, then cut from one past it: the + 1 skips the character itself. npos is the value find() returns when it finds nothing, and the largest value a string position can hold:
std::string setting = "name=Ada";
std::size_t eq = setting.find('=');
if (eq != std::string::npos) {
std::cout << setting.substr(eq + 1) << '\n';
}
std::string path = "photos/2026/beach.jpg";
std::size_t dot = path.rfind('.'); // the last dot
std::size_t slash = path.rfind('/'); // the last slash
if (dot != std::string::npos) {
std::cout << path.substr(dot + 1) << '\n';
}
if (slash != std::string::npos) {
std::cout << path.substr(slash + 1) << '\n';
}
std::string line = "[warning] disk almost full";
std::size_t open_pos = line.find('[');
std::size_t close_pos = line.find(']', open_pos);
if (open_pos != std::string::npos &&
close_pos != std::string::npos) {
std::size_t start = open_pos + 1;
std::cout << line.substr(start, close_pos - start) << '\n';
}— end example]
It printed Ada, jpg, beach.jpg and warning. Between two characters, the length is the closing position minus the first position you keep: the same end - start as before. The text before a character at position i is s.substr(0, i), because i is also the number of characters ahead of it. To cut a whole line into fields rather than take one piece, std::getline with a delimiter reads one field at a time.
Why does substr(find(x) + 1) return the whole string?
Because the character was not there: find() returned npos, npos + 1 wraps around to 0, and substr() starts at the beginning. This is the line that skips the check:
std::string text = "disk almost full"; // no colon
// find returns npos; npos + 1 wraps around to 0
std::string after = text.substr(text.find(':') + 1);
std::cout << '[' << after << "]\n";
std::cout << std::boolalpha;
std::cout << (text.find(':') == std::string::npos) << '\n';
std::cout << text.find(':') + 1 << '\n';— end example]
It printed:
[disk almost full]
true
0— end example]
None of the compilers warned, not MSVC at /W4 and not GCC or Clang with -Wall -Wextra, and all four printed the whole text. Nothing fails: the program carries on with the wrong value. The if (eq != std:: in the code above is the fix.
What happens when pos or count is too large?
A count that runs past the end is cut short, and a pos equal to size() gives an empty string. Only a pos greater than size() is an error: substr() throws std::out_of_range (substr on cppreference). On the ten-character date:
std::cout << '[' << date.substr(8, 100) << "]\n"; // cut short
std::cout << '[' << date.substr(date.size()) << "]\n"; // empty
try {
std::cout << date.substr(11) << '\n'; // 11 > size(): throws
} catch (const std::out_of_range& e) {
std::cout << "out_of_range: " << e.what() << '\n';
}— end example]
All four printed [12] and [], then reached the catch. The empty case is why cutting after a delimiter that ends the string, as in "name=", gives an empty value instead of an exception. The text of what() differs from one standard library to the next, so catch the type and do not test the message. The samples include <stdexcept> for std::out_of_range.
How do you check if a string contains a substring?
Compare find() with npos: s.find(text) != std:: is true when text occurs anywhere in s. Since C++23, s.contains(text) makes the same test, and since C++20, starts_with() and ends_with() check the two ends:
std::cout << std::boolalpha;
std::cout << (date.find("-11-") != std::string::npos) << '\n';
std::cout << date.starts_with("2026") << '\n'; // C++20: starts_with
std::cout << date.ends_with("-12") << '\n'; // C++20: ends_with
std::cout << date.contains("-11-") << '\n'; // C++23: contains— end example]
Built as C++23, all four printed true four times. As C++20, contains compiled on none of them, and as C++17 neither did starts_with or ends_with. Before C++23, write the find test: contains is defined as exactly that comparison.
How do you take a substring without copying?
Call substr() on a std::string_view, a type that refers to characters stored somewhere else instead of holding its own copy. Its substr() takes the same two arguments and throws the same std::out_of_range, but returns a view of the original characters:
std::string_view year_of(std::string_view date) {
return date.substr(0, 4); // a view of date's characters
}
// ...
std::string date = "2026-11-12";
std::string_view view = date; // no copy
std::cout << view.substr(0, 4) << '\n'; // no copy either
std::cout << year_of(date) << '\n';
std::cout << year_of("2026-11-12") << '\n';— end example]
All four printed 2026 three times, from the string and from the literal alike. The sample also compared addresses: the view’s substr() pointed into date’s own characters, where date.substr(0, 4) pointed at a copy.
A view does not keep those characters alive, so do not keep one after its string is gone. That includes std::: date.substr() makes a copy anyway, and the view refers to that temporary string, which is gone at the end of the line. std::string_view arrived in C++17; Since When’s C++17 table lists the first compiler releases with it.
substr() at a glance
| You want | Write |
|---|---|
| The first n characters | s.substr(0, n) |
| The last n characters | s.substr(s.size() - n), once n <= s.size() |
| From a position to the end | s.substr(pos) |
| n characters from a position | s.substr(pos, n) |
| From a up to, not including, b | s.substr(a, b - a) |
| The text after a character | s.substr(i + 1), once i = s.find(c) is not npos |
| The text before a character | s.substr(0, i), with the same check |
| Whether s contains t | s.find(t) != std::; C++23: s.contains(t) |
| A piece with no copy | substr() on a std::string_view |
The Standard library section has the answers on the library’s other types and functions.
Questions
Does substr() change the original string?
No. s.substr() returns a new string that holds a copy of the characters, and s stays as it was. To work on the same characters without a copy, call substr() on a std::string_view instead.
Is the second parameter of substr() the end index in C++?
No, it is a length: s.substr(5, 2) takes two characters starting at position 5. To cut from a start index up to, not including, an end index, the way Java and Python count, write s.substr(start, end - start).
What does substr() do if count is larger than the string?
It stops at the end of the string and returns the characters that are there, with no error: on “2026-11-12″, substr(8, 100) returned “12″. Only a starting position greater than size() throws std::out_of_range.
What is the time complexity of substr()?
Linear in the length of the substring: substr() copies each character it returns, so its cost does not depend on how long the original string is. std:: takes constant time, because it copies no characters.
How do you split a string in C++?
Loop with find() and substr(): find the next delimiter from the current position, cut the piece before it, and start again one past it. Or let std::getline do the loop: given a delimiter as its third argument, it reads one field at a time from a stream, such as a std::istringstream made from the string.
Which header does substr() need?
<string>, the header that declares std::string, of which substr() is a member function. Some tutorials name <string.h>, which is the wrong header. The samples that catch std::out_of_range include <stdexcept> as well.
Sources
- std::basic_string::substrcppreference.com
- [string.substr] basic_string::substrC++ standard working draft
- std::basic_string::findcppreference.com
- std::basic_string::nposcppreference.com
- std::basic_string_view::substrcppreference.com
- std::basic_string::containscppreference.com