DEV Community

Sandor Dargo
Sandor Dargo

Posted on • Originally published at sandordargo.com

What are string_views and why should we use them?

std::string_view has been introduced by C++17 and its purpose is to provide read-only access to character sequences. It potentially replaces const string& parameters and offers a significant performance gain. Let's delve into some details.

How is it implemented?

A typical implementation of a string_view needs two pieces of information. A pointer to the character sequence and its length. The character sequence can be both a C++ or a C-string. After all, std::string_view is a non-owning reference to a string.

If we check the major implementations, we can observe that indeed all of them implemented string_view by storing a pointer to the string data and the size of the string. You can have a look at the implementations here:

Why is it useful?

This type is particularly useful! It's quite cheap to copy it as it only needs the above-mentioned copy and its length. It's so cheap to copy it that you should never see a string_view passed around by reference. It's so cheap to copy that makes const string& parameters superfluous in the vast majority of the cases.

If a function doesn't need to take ownership of its string argument and it only performs read operations (plus some modifications, to be discussed later) then you can use a string_view instead.

When you need to own a character sequence, you should use a std::string as the Core Guidelines reminds us. Otherwise, string_views provide an easy way to get a view of strings no matter how they are allocated or stored. By that I mean that it doesn't matter whether the underlying string has an implicit null termination (std::string) or not (const char *), string_view will be useable.

If for some reason though you need that implicit null termination, you still must stick with a const string&.

If you want to get a bit more information about the performance of std::string_view against std::string, I highly recommend checking out this article from ModernesC++. In the last sections, Rainer Grimm shows the time difference it takes to create substrings either with std::string::substr or with std::string_view::substr and the results are just amazing.

The difference depends a lot on the size of the substring which is due to the cost allocation a string needs and also due to small string optimization eliminating this need. All in all, the bigger substrings we create the more we save. Having -O3 turned on for smaller strings, Rainer achieved an improvement of almost 10x at least, but for big enough strings it was beyond an astonishing x7500 improvement.

What API does string_view offers?

As mentioned ealier, even though string_view is not owning the underlying string, it offers some modifying operations. I'd say

  • std::string_view::swap is obvious, it simply exchanges views between two string_views.
  • remove_prefix and remove_suffix are more interesting, how is that possible?

These modifiers take a number (size_type) n to be removed. As we discussed earlier, a string_view usually has two data members. A pointer to the underlying character list and its size. In order to remove the suffix, so the end of the string, it's enough to decrease the size data member by n. And in order to remove the prefix, besides decreasing the size, the pointer pointing at the character list should also be increased. It's as easy, assuming that the characters are stored in a contiguous memory area.

#include <iostream>
#include <string_view>


int main() {
    std::string_view sv{"here this is a string_view example"};
    std::cout << sv << '\n';
    sv.remove_prefix(5);
    std::cout << sv << '\n';
    sv.remove_suffix(8);
    std::cout << sv << '\n';
}
/*
here this is a string_view example
this is a string_view example
this is a string_view
*/
Enter fullscreen mode Exit fullscreen mode

Apart from these, the string_view offered from the beginning the following functionalities:

  • copy
  • substr
  • compare
  • a bit set of find methods

Let's have a look at copy and compare!

std::string_view::copy

I wanted to zoom in on this method because when I first saw, I asked myself the question what do we copy there? And from there?

std::string_view::copy takes three parameters with the last one having a default value. The first parameter is the destination, the second one is the length of the substring you want to copy and the third one is the starting point. If you don't specify the last one, it's the beginning of the string by default.

So with std::string_view::copy we copy from the underlying view to somewhere else.

Where can we copy? It can be any container of characters. Here are a few examples.

#include <array>
#include <iostream>
#include <iomanip>
#include <string_view>


int main() {
    std::string_view sv{"here this is a string_view example"};
    std::array<char, 8> destinationArray{};


    sv.copy(destinationArray.data(), 4);
    for (auto c: destinationArray) {
        std::cout << c;
    }
    std::cout << '\n';

    std::string destinationStringNoSpace;
    sv.copy(destinationStringNoSpace.data(), 9);
    std::cout << destinationStringNoSpace << '\n';

    std::string destinationStringWithSpace(' ', 9);
    sv.copy(destinationStringWithSpace.data(), 9);
    std::cout << destinationStringWithSpace << '\n';
}
Enter fullscreen mode Exit fullscreen mode

It's worth noting that we can copy to char*, therefore we always pass in the result of the data() accessor. It's also worth nothing that we have to make sure that a string is big enough. And reserve is not good enough as it only makes sure that there is enough space to grow, not that there is space initialized.

std::string_view::compare

I wanted to zoom in on std::string_view::compare as it's always worth having a look at comparisons that return an integer value? What do they mean?

But having a look at the available signatures poses some other questions.

There are two straightforward ones. The compare member method can be called with either another string_view or with a const char*. But that is not all! You don't have to compare the full string_view. You might pass in a starting position and a count for the underlying script_view, they precede the other character sequence.

In addition, if you compare with another string_view, you can pass in the starting position and the size for the other view too. If you compare with a const char*, you cannot define the starting position, but you can still pass in the size.

And what are the available return values?

  • 0 if both are equal.
  • You get a positive value if the underlying string is greater.
  • You get a negative value if the other string is greater.

Let's have a look at some examples.

#include <string_view>

int main() {
    using std::operator""sv;

    static_assert( "abc"sv.compare("abcd"sv) < 0 ); // Other is greater
    static_assert( "abcd"sv.compare(0, 3, "abcd"sv) < 0 ); // Other is greater
    static_assert( "abcd"sv.compare(1, 3, "abcd"sv) > 0 ); // This is greater
    static_assert( "abcd"sv.compare(1, 3, "abcd"sv, 1, 3) == 0 ); // Both are equal
    static_assert( "abcd"sv.compare(1, 3, "bcde", 3) == 0 ); // Both are equal
    static_assert( "abcd"sv.compare("abc"sv) > 0 ); // This is greater
    static_assert( "abc"sv.compare("abc"sv) == 0 ); // Both are equal
    static_assert( ""sv.compare(""sv) == 0 );// Both are equal
}
Enter fullscreen mode Exit fullscreen mode

Novelties of string_view in C++23/C++20

But since its introduction in C++17, string_view has received some new functionalities in both C++20 and 23.

starts_with / ends_with added in C++20

These two queries were added to string_view in C++20. They help us to write more expressive code. We can simply call them to check whether a string starts or ends with a given substring. Look at the below example to see how it simplifies life.

#include <iostream>
#include <iomanip>
#include <string_view>


int main() {
    std::string_view sv{"here this is a string_view example"};

    if (sv.starts_with("here")) {
        std::cout << std::quoted(sv) << " starts with \"here\"\n";
    }

    if (!sv.ends_with("view")) {
        std::cout << std::quoted(sv) << " does not end with \"view\"\n";
    }
}
Enter fullscreen mode Exit fullscreen mode

How much does it simplify life? Just check out this or this article and you'll see! This is just a super addition!

std::string_view now have contains

One of C++20's useful addition to maps were the contains member function. We could replace the cumbersome to read query of myMap.find(key) != myMap.end() with the very easy to understand myMap.contains(key). With C++23, std::string and std::string_view will have similar capabilities. You can call contains() with either a string or a character and it will return true or false depending on whether the queried string or string_view contains the input parameter.

#include <iostream>
#include <string>
#include <iomanip>

int main() {
    std::string s{"there is a needle in the haystack"};
    std::string_view sv{"acdef"};

    if (s.contains("needle")) {
        std::cout << "we found a needle in: " << std::quoted(s) << '\n';
    }

    if (!sv.contains('b')) {
        std::cout << "we did not find a 'b' in: " << std::quoted(sv) << '\n';
    }
}
/*
we found a needle in: "there is a needle in the haystack"
we did not find a 'b' in: "acdef"
*/
Enter fullscreen mode Exit fullscreen mode

Build std::string_view from ranges

With C++23, our favourite string_view doesn't only loses a constructor (the overload with a nullptr gets deleted), but also receives a new one. Soon, we'll be able to construct one out of a range directly.

So far, if we wanted to create a string_view out of a "range", we had to invoke the constructor with a begin and and end iterators: std::string_view sv(myRange.begin(), myRange.end());. Now we'll be able to directly construct a string_view based on a range: std::string_view sv(myRange);.

Require span & basic_string_view to be TriviallyCopyable

P2251R1 updates the requirements the standard has for std::span and std::string_view. Starting from C++23 they must satisfy the TriviallyCopyable concepts.

As both of these objects already have default copy assignment operators and constructs and also destructors and besides they only expose a size_t and a raw pointer, it is implied that these types can be trivially copyable and in fact, the major compilers already implemented them as so.

Ensuring this trait for the future makes sure developers can continue depending on these characteristics and less courageous developers can start using them as such for example in heterogeneous computing.

Conclusion

In this post, we discussed what string_views are and howthey simplify our lives. We saw that they don't just offer superior performance due to fewer copies but they also provide an easy-to-use interface that gets better with each version.

Have you started using more and more the string_view instead of const string& in your projects?

Connect deeper

If you liked this article, please

Top comments (2)

Collapse
 
pgradot profile image
Pierre Gradot

You know I wrote an article about this class (dev.to/younup/comprendre-std-strin...) and you may probably know what I am gonna say ;)

I will say that you don't mention the dangling-view issue: as it is a view on some data, you must be sure that those data are still alive when you use the view.

Some time has passed since I wrote my article, and we (our team) have gained experience with std::string_view: the fact that you have no guarantee that they end with a \0 can lead so subtle bugs. This is especially true in our case because we interact we quite a lot of C code and simply calling data() is often a bad idea.

As the end of the day, I am less enthusiastic now than 1.5 year ago (but still enthusiastic of course ;) )

Collapse
 
sandordargo profile image
Sandor Dargo

Thanks for your comment. Indeed, I think I mentioned that if you need that \0 you better stick with the type that guarantees it.

I'm usually freaked out when I see .data() in the code :)