7

I've been adding std::string_views to some old code for representing string like config params, as it provides a read only view, which is faster due to no need for copying.

However, one cannot concatenate two string_view together as the operator+ isn't defined. I see this question has a couple answers stating its an oversight and there is a proposal in for adding that in. However, that is for adding a string and a string_view, presumably if that gets implemented, the resulting concatenation would be a std::string

Would adding two string_view also fall in the same category? And if not, why shouldn't adding two string_view be supported?

Sample

std::string_view s1{"concate"};
std::string_view s2{"nate"};
std::string_view s3{s1 + s2};

And here's the error

error: no match for 'operator+' (operand types are 'std::string_view' {aka 'std::basic_string_view<char>'} and 'std::string_view' {aka 'std::basic_string_view<char>'})
Enlico
  • 23,259
  • 6
  • 48
  • 102
simplename
  • 717
  • 7
  • 15
  • 5
    A `string_view` does not _own_ the data. It must be stored somewhere. For concatenating, use a `std::string`. – Ted Lyngmo Aug 24 '22 at 19:07
  • 1
    The "oversight" mentioned in the other question is the oversight of adding a string_view to a string, and not adding two string_views together. There's no proposal to implement like this for the simple reason that it's logically impossible to do anything like this, for fundamental reasons. – Sam Varshavchik Aug 24 '22 at 19:07

4 Answers4

10

A view is similar to a span in that it does not own the data, as the name implies it is just a view of the data. To concatenate the string views you'd first need to construct a std::string then you can concatenate.

std::string s3 = std::string(s1) + std::string(s2);

Note that s3 will be a std::string not a std::string_view since it would own this data.

Cory Kramer
  • 114,268
  • 16
  • 167
  • 218
  • Alt. `std::string s3 = std::string(s1) += s2;` - I'm assuming it'll do one allocation less if `s2` is not short enough for short string optimization. – Ted Lyngmo Aug 24 '22 at 19:19
  • So to store `s3` as a `string_view` I should `std::string_view s3{std::string(s1) + std::string(s2)};` – simplename Aug 24 '22 at 19:21
  • 1
    @simplename No, where will the data be stored then? As soon as the `std::string` goes out of scope, any `string_view` you have to it will not be useful anymore. You must store the `std::string`. You can make as many `string_view`s you like to it - but you need to keep it alive. – Ted Lyngmo Aug 24 '22 at 19:21
  • 2
    @simplename I’ll post this as a full answer if I have the time but if you’re that determined to keep everything as a `sv` for a long as possible you could create a simple class that aggregates the `sv`s. But frankly I wouldn’t worry about that unless you’re absolutely sure this is slowing down your system and that the solution is faster – Taekahn Aug 24 '22 at 19:42
6

A std::string_view is an alias for std::basic_string_view<char>, which is a std::basic_string_view templated on a specific type of character, i.e. char.

But what does it look like?

Beside the fairly large number of useful member functions such as find, substr, and others (maybe it's an ordinary number, if compared to other container/string-like things offered by the STL), std::basic_string_view<_CharT>, with _CharT being the generic char-like type, has just 2 data members,

// directly from my /usr/include/c++/12.2.0/string_view
      size_t        _M_len;
      const _CharT* _M_str;

i.e. a constant pointer to _CharT to indicate where the view starts, and a size_t (an appropriate type of number) to indicate how long the view is starting from _M_str's pointee.

In other words, a string view just knows where it starts and how long it is, so it represents a sequence of char-like entities which are consecutive in memory. With just two such members, you can't represent a string which is made up of non-contiguous substrings.

Yet in other words, if you want to create a std::string_view, you need to be able to tell how many chars it is long and from which position. Can you tell where s1 + s2 would have to start and how many characters it should be long? Think about it: you can't, becase s1 and s2 are not adjacent.

Maybe a diagram can help.

Assume these lines of code

std::string s1{"hello"};
std::string s2{"world"};

s1 and s2 are totally unrelated objects, as far as their memory location is concerned; here is what they looks like:

                           &s2[0]
                             |
                             | &s2[1]
                             |   |
&s1[0]                       |   | &s2[2]
  |                          |   |   |
  | &s1[1]                   |   |   | &s2[3]
  |   |                      |   |   |   |
  |   | &s1[2]               |   |   |   | &s2[4]
  |   |   |                  |   |   |   |   |
  |   |   | &s1[3]           v   v   v   v   v
  |   |   |   |            +---+---+---+---+---+
  |   |   |   | &s1[4]     | w | o | r | l | d |
  |   |   |   |   |        +---+---+---+---+---+
  v   v   v   v   v
+---+---+---+---+---+
| h | e | l | l | o |
+---+---+---+---+---+

I've intentionally drawn them misaligned to mean that &s1[0], the memory location where s1 starts, and &s2[0], the memory location where s2 starts, have nothing to do with each other.

Now, imagine you create two string views like this:

std::string_view sv1{s1};
std::string_view sv2(s2.begin() + 1, s2.begin() + 4);

Here's what they will look like, in terms of the two implementation-defined members _M_str and _M_len:

                                &s2[0]
                                  |
                                  | &s2[1]
                                  |   |
     &s1[0]                       |   | &s2[2]
       |                          |   |   |
       | &s1[1]                   |   |   | &s2[3]
       |   |                      |   |   |   |
       |   | &s1[2]               |   |   |   | &s2[4]
       |   |   |                  |   |   |   |   |
       |   |   | &s1[3]           v   v   v   v   v
       |   |   |   |            +---+---+---+---+---+
       |   |   |   | &s1[4]     | w | o | r | l | d |
       |   |   |   |   |        +---+---+---+---+---+
       v   v   v   v   v            · ^         ·
     +---+---+---+---+---+          · |         ·
     | h | e | l | l | o |        +---+         ·
     +---+---+---+---+---+        | ·           ·
     · ^                 ·        | · s2._M_len ·
     · |                 ·        | <----------->
   +---+                 ·        |
   | ·                   ·        +-- s2._M_str
   | ·       s1._M_len   ·
   | <------------------->
   |
   +-------- s1._M_str

Given the above, can you see what's wrong with expecting that

std::string_view s3{s1 + s2};

works?

How can you possible define s3._M_str and s3._M_len (based on s1._M_str, s1._M_len, s2._M_str, and s2._M_len), such that they represent a view on "helloworld"?

You can't because "hello" and "world" are located in two unrelated areas of memory.

Enlico
  • 23,259
  • 6
  • 48
  • 102
6

std::string_view does not own any data, it is only a view. If you want to join two views to get a joined view, you can use boost::join() from the Boost library. But result type will be not a std::string_view.

#include <iostream>
#include <string_view>
#include <boost/range.hpp>
#include <boost/range/join.hpp>

void test()
{
    std::string_view s1{"hello, "}, s2{"world"};
    auto joined = boost::join(s1, s2);

    // print joined string
    std::copy(joined.begin(), joined.end(), std::ostream_iterator(std::cout, ""));
    std::cout << std::endl;

    // other method to print
    for (auto c : joined) std::cout << c;
    std::cout << std::endl;
}

C++23 has joined ranges in the standard library with the name of std::ranges::views::join_with_view

#include <iostream>
#include <ranges>
#include <string_view>

void test()
{
    std::string_view s1{"hello, "}, s2{"world"};
    auto joined = std::ranges::views::join_with_view(s1, s2);

    for (auto c : joined) std::cout << c;
    std::cout << std::endl;
}
Raider
  • 197
  • 2
  • 10
  • That C++23 code is incorrect, as `join_with_view` is in the `std::ranges` namespace, not `std::ranges::views`. I get a constraint failure when I fixed that. In any case, we don't want `"world"` _between each character_ of `"hello, "`. Luckily, there's a simpler C++20 version: `std::views::join(std::array{s1, s2})`. – Toby Speight Aug 03 '23 at 07:46
  • I've added my own answer with verified working code. – Toby Speight Aug 03 '23 at 08:13
0

A std::string_view is a lightweight, non-owning view of the characters.

To get a view that concatenates multiple string views, we can use the join view adapter that was introduced in C++20:

    auto const joined = std::views::join(std::array{s1, s2});

This gives us a view object that can be iterated over using standard algorithms or range-based for. It can be converted to a std::string object (but not directly to a std::string_view as that requires us to copy the contents somewhere to make them contiguous).

Full demo:

#include <algorithm>
#include <array>
#include <ranges>
#include <string_view>

int main()
{
    using namespace std::string_view_literals;

    std::string_view s1{"concate"};
    std::string_view s2{"nate"};

    return !std::ranges::equal(std::views::join(std::array{s1, s2}),
                               std::string_view{"concatenate"});
}
Toby Speight
  • 27,591
  • 48
  • 66
  • 103