simplename
simplename

Reputation: 919

Concatenating string_view objects

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>'})

Upvotes: 15

Views: 23927

Answers (5)

Edison von Myosotis
Edison von Myosotis

Reputation: 619

This is a convenient function to concatenate an arbitrary number of objects that are convertible to a string_view:

template<typename String, typename ... Viewables>
    requires std::same_as<String, std::basic_string<typename String::value_type, typename String::traits_type, typename String::allocator_type>>
        && (std::convertible_to<Viewables, std::basic_string_view<typename String::value_type, typename String::traits_type>> && ...)
inline String sv_concat( Viewables const &... viewables )
{
    using namespace std;
    String str;
    if constexpr( sizeof ...(Viewables) )
        [&]( auto ... views )
        {
#if defined(__cpp_lib_string_resize_and_overwrite)
            str.resize_and_overwrite( (views.length() + ...), [&]( String::value_type *p, size_t n )
                {
                    auto where = span( p, n ).begin();
                    ((where = copy( views.begin(), views.end(), where )), ...);
                    return n;
                } );
#else
            str.reserve( (views.length() + ...) );
            ((str += views), ...);
#endif
        }( basic_string_view<typename String::value_type, typename String::traits_type>( viewables ) ... );
    return str;
}

With that you can concatenate like this:

string
    str( "hello" ),
    strX( sv_concat<string>( str, " world" );

The resulting concatenation has maximum efficiency if the strings are dynamic.

Upvotes: 1

Enlico
Enlico

Reputation: 28416

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 contiguous 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.

Upvotes: 11

Toby Speight
Toby Speight

Reputation: 30831

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()
{
    const std::string_view s1{"con"};
    const std::string_view s2{"cate"};
    const std::string_view s3{"nate"};

    return !std::ranges::equal(std::views::join(std::array{s1, s2, s3}),
                               std::string_view{"concatenate"});
}

Upvotes: 6

Raider
Raider

Reputation: 317

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

Upvotes: 6

Cory Kramer
Cory Kramer

Reputation: 117876

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.

Upvotes: 14

Related Questions