Last Update:
Six Handy Operations for String Processing in C++20/23
Table of Contents
In this article, we’ll explore six practical string processing operations introduced in C++20 and C++23. These features represent an evolution in this crucial area, covering a spectrum of operations from searching and appending to creation and stream handling.
Let’s start with a simple yet long-awaited feature…
1. contains()
, C++23
Finally, after decades of standardization, we have a super easy way to check if there’s one string inside the other. No need to use .find(str) != npos
!
Before C++23:
#include <string>
#include <iostream>
int main() {
const std::string url = "https://isocpp.org";
if (url.find("https") != std::string::npos &&
url.find(".org") != std::string::npos &&
url.find("isocpp") != std::string::npos)
std::cout << "You're using the correct site!\n";
}
And now, thanks to the proposal: P1679R3:
#include <string>
#include <iostream>
int main(){
const std::string url = "https://isocpp.org";
if (url.contains("https") &&
url.contains(".org") &&
url.contains("isocpp"))
std::cout << "you're using the correct site!\n";
}
Run at Compiler Explorer
As you can see, the new approach with contains()
is more straightforward and intuitive.
2. starts_with()
, ends_with()
, C++20 & C++23
While contains()
checks the whole string, in C++20, we got functions targeted for prefixes and suffixes. Have a look:
#include <string>
#include <iostream>
int main(){
const std::string url = "https://isocpp.org";
if (url.starts_with("https") && url.ends_with(".org"))
std::cout << "you're using the correct site!\n";
}
See at Compiler Explorer
It’s good to know that in C++23, those algorithms are also extended to ranges, so you’ll be able to write:
std::ranges::starts_with("Hello World", "Hello"sv)
std::vector nums { 1, 2, 3, 4};
std::ranges::ends_with(v, {3, 4});
The range versions are interesting, as they allow passing projections, and thus, we can be more flexible about the check between separate letters/elements.
And you can see more about the string functions in my separate article: starts_with() and ends_with() for Strings in C++20 - C++ Stories
3. Range operations, C++23
Thanks to ranges::to<>
, many containers from the Standard Library got new ways to assign, append, or insert ranges.
The string class got the following members:
std::basic_string<CharT,Traits,Allocator>::insert_range
std::basic_string<CharT,Traits,Allocator>::append_range
std::basic_string<CharT,Traits,Allocator>::replace_with_range
For example (stealing a cool example from C++Reference):
#include <iostream>
#include <iterator>
#include <string>
int main() {
const auto source = {'l', 'i', 'b', '_'};
std::string target{"__cpp_containers_ranges"};
const auto pos = target.find("container");
auto iter = std::next(target.begin(), pos);
#ifdef __cpp_lib_containers_ranges
target.insert_range(iter, source);
#else
target.insert(iter, source.begin(), source.end());
#endif
std::cout << target;
}
Run at Compiler Explorere
See more in the paper P1206R7
We can also write a bit convoluted example with ranges::to
:
#include <iostream>
#include <ranges>
#include <string>
int main() {
auto str = std::views::iota('a', 'g')
| std::views::transform([](auto const v){ return v + 2; })
| std::ranges::to<std::string>();
std::cout << str << '\n';
auto str2 = str | std::views::take(3) | std::ranges::to<std::string>();
std::cout << str2 << '\n';
}
The output:
cdefgh
cde
Run at Compiler Explorer
4. stringstream::view()
, C++20
If you have to work with streams, and stringstream
in particular, you may already know they might be pretty slow. Yet, in Modern C++, there are a couple of ways to speed things up a bit. For example, in C++20, you have the view()
member function. This can be used as an alternative to str()
. In short, rather than creating a copy of the internal string, you’ll get a view, so there is no need to dynamically allocate memory.
Have a look:
#include <iostream>
#include <sstream>
void* operator new(std::size_t sz){
std::cout << "Allocating: " << sz << '\n';
return std::malloc(sz);
}
int main() {
std::cout << "start...\n";
std::stringstream str;
str << 42;
str << " Hello C++20/23 Programming World";
std::cout << "print with str()\n";
std::cout << str.str() << '\n';
std::cout << "another try...\n";
std::cout << str.view() << '\n';
}
Possible output:
start...
Allocating: 513
print with str()
Allocating: 36
42 Hello C++20/23 Programming World
another try...
42 Hello C++20/23 Programming World
Run at Compile Explorer
When the example calls str.str()
, you can see that there’s extra memory allocation for 36 bytes. But in the line with view()
, there’s no allocation.
5. rvalue string constructor for stringstream
, C++20
But there’s more to stringstream
in C++20! For example, there’s an extra constructor that can take an rvalue reference to the string object, and thus it might not require an additional copy:
#include <iostream>
#include <sstream>
void* operator new(std::size_t sz){
std::cout << "Allocating: " << sz << '\n';
return std::malloc(sz);
}
int main() {
std::cout << "start...\n";
std::stringstream str { std::string("hello C++ programming World")};
}
When we compile the code with the C++17 mode (See at Compiler Explorer) you’ll see the following output:
start...
Allocating: 28
Allocating: 28
There’s clearly a copy of the string.
But try compiling that with the std=c++20
flag….
6. spanstream
from C++23
If you need to use stringstream
, and the previous techniques didn’t bring you any gains, and you want complete control over the internal memory, in C++23, you can easily update your code with spanstream
!
In short, this is a stream class that operates on spans (also from C++20). Have a look at this simplistic example:
#include <iostream>
#include <sstream>
#include <spanstream> // << new headeer!
void* operator new(std::size_t sz){
std::cout << "Allocating: " << sz << '\n';
return std::malloc(sz);
}
int main() {
std::cout << "start...\n";
std::stringstream ss;
ss << "one string that doesn't fit into SSO";
ss << "another string that hopefully won't fit";
std::cout << "spanstream:\n";
char buffer[128] { 0 };
std::span<char> spanBuffer(buffer);
std::basic_spanstream<char> ss2(spanBuffer);
ss2 << "one string that doesn't fit into SSO";
ss2 << "another string that hopefully won't fit";
std::cout << buffer;
}
The output:
start...
Allocating: 513
spanstream:
one string that doesn't fit into SSOanother string that hopefully won't fit
In the first part - ss
- the example shows a regular stringstream
. Our simplistic memory allocation tracker points out 513 bytes had to be allocated.
On the other hand, in the second part, we have the new type, spanstream
, which uses a preallocated buffer (a regular array), and this buffer is used as the internal memory for the stream object.
Conclusion
In this text, we explored a range of functionalities, from basic searching with contains()
to sophisticated stream operations with spanstream
, each bringing its unique advantage to the table. These enhancements can significantly simplify everyday programming tasks.
But there’s more in C++20/23:
- Prohibiting
std::basic_string
andstd::basic_string_view
construction fromnullptr
- P2166 std::erase
for containers including strings- Better Unicode support, P1041R4
Back to you
- What string operations do you use the most often?
- Have you played with the latest additions to string/stream processing in C++20/23?
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: