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;
}

See @Compiler Explorer

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 and std::basic_string_view construction from nullptr - 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?