连接string_view对象

10
我一直在将std::string_view添加到一些旧代码中,用于表示字符串配置参数,因为它提供了一个只读视图,由于不需要复制,所以速度更快。
然而,两个string_view不能连接在一起,因为未定义operator+。我看到这个问题有几个答案表明这是一个疏忽,有一个提案正在添加该功能。但是,那是为了添加一个string和一个string_view,假设实现了这个功能,结果连接将是一个std::string
两个string_view的添加也会属于同一类吗?如果不是,为什么不支持添加两个string_view
示例
std::string_view s1{"concate"};
std::string_view s2{"nate"};
std::string_view s3{s1 + s2};

这里是错误信息

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

5
string_view不拥有数据,它必须存储在某个地方。如果要连接字符串,请使用std::string - Ted Lyngmo
2
在其他问题中提到的“疏忽”是将string_view添加到字符串中,而不是将两个string_views相加。没有提出这样的实现建议,因为基本原理上逻辑上不可能做到这样的事情。 - Sam Varshavchik
4个回答

13

视图类似于跨度,因为它不拥有数据,正如名称所示,它只是数据的一种视图。要连接字符串视图,您首先需要构造一个std::string,然后才能连接。

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

请注意,s3将是一个std::string而不是std::string_view,因为它将拥有这些数据。

如果s2不够短以进行短字符串优化,那么使用以下方式会比原方法节省一次内存分配: std::string s3 = std::string(s1) += s2; - Ted Lyngmo
那么要将s3存储为string_view,我应该这样做:std::string_view s3{std::string(s1) + std::string(s2)}; - simplename
3
@simplename 不是的,那么数据将存储在哪里?一旦 std::string 超出作用域,任何对它的 string_view 都将不再有用。你必须存储 std::string。你可以创建任意多个 string_view,但需要保持其有效。 - Ted Lyngmo
2
@simplename 如果我有时间,我会将此作为完整答案发布,但如果您非常确定要尽可能长时间地保留所有内容作为sv,则可以创建一个简单的类来聚合这些sv。但坦率地说,除非您绝对确定这会减慢系统速度并且解决方案更快,否则我不会担心这个问题。 - Taekahn

7

std::string_viewstd::basic_string_view<char>的别名,它是一个以特定字符类型char为模板参数的std::basic_string_view

那么它长什么样呢?

除了有一些有用的成员函数(例如findsubstr等),其数量与STL提供的其他容器/类似字符串的东西相比可能还算普通,std::basic_string_view<_CharT>只有两个数据成员,其中_CharT是通用的char-like类型。

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

即一个指向_CharT的常量指针,用于表示视图的起始位置,和一个size_t(一种适当类型的数字),用于表示从_M_str的指针开始视图的长度。

换句话说,字符串视图只知道它在哪里开始以及它有多长,因此它表示了内存中连续的类似于char实体的序列。仅凭两个成员,您无法表示由非连续子字符串组成的字符串。

换言之,如果要创建std::string_view,则需要能够告诉它有多少个char并且从哪个位置开始。您能告诉s1 + s2应该从哪里开始,并且长度应该是多少吗?好好想想:您不能,因为s1s2不是相邻的。

也许一个图可以帮助理解。

假设有以下代码行:

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

s1s2在内存位置上完全没有关联,它们看起来是这样的:

                           &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 |
+---+---+---+---+---+

我故意将它们绘制为不对齐的,这是为了说明&s1[0],也就是s1开始的内存位置和&s2[0],也就是s2开始的内存位置互不相关。

现在,想象你创建了两个字符串视图:

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

下面是针对两个实现定义成员变量 _M_str_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

考虑到以上情况,你能看出期望什么有什么问题吗?
std::string_view s3{s1 + s2};

这个方案可行吗?

如何基于s1._M_strs1._M_lens2._M_strs2._M_len来定义s3._M_strs3._M_len,使它们表示对"helloworld"的视图?

不可能,因为"hello""world"在内存的两个不相关区域中。


1
虽然这是有道理的,但它并不能解释为什么你不能将一个string和一个string_view连接起来并得到一个string。也许不允许这样做的意图是因为这样会不清楚是要产生一个连接的视图(就像C++20中的std::views::join一样,它不是一个string_view,因为它不是连续的),还是一个新的string,它在自己的存储中保存了连接的结果。 - undefined
@FlorianWinter,如果通过将std::stringstd::string_view连接起来,你的意思是a_string + a_string_view,那么允许这样做就需要将a_string_view通过构造函数(10)转换为std::string,但是该构造函数是explicit的,所以这是不可能的。 - undefined
不完全是这样,因为将 string_view 转换为 string 将会导致不必要的内存分配。连接一个 string 和一个 string_view 不应该需要超过一次内存分配。此外,如果这个 string 是一个右值,那么它可以被重用,例如,它的存储可能已经具备了所需的容量来容纳结果,根本不需要进行内存分配。虽然可以以这种方式实现,但我认为标准为什么不支持这个操作给出了一个正式的原因。 - undefined
此外,这种连接的意图可能会有所不同。预期的结果可能是一个具有自己存储的“字符串”,但也可能是一个视图(但不是“string_view”),这就是“std::views::join”的作用。当然,连接的结果不能是一个“string_view”(但它可以是一个视图,就像C++20的范围库一样)。 - undefined

6

std::string_view 不拥有任何数据,它只是一个视图。如果你想要连接两个视图得到一个连接的视图,可以使用 Boost 库中的 boost::join()。但是结果类型不会是一个 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将ranges加入标准库,命名为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;
}

那段C++23的代码是错误的,因为join_with_viewstd::ranges命名空间中,而不是std::ranges::views。当我修复了这个问题后,出现了约束失败。无论如何,我们不希望在“hello, ”的每个字符之间有“world”。幸运的是,有一个更简单的C++20版本:std::views::join(std::array{s1, s2}) - Toby Speight
我已经添加了自己的答案,并附上了经过验证的可工作代码。 - Toby Speight

2
一个 `std::string_view` 是字符的轻量级、非拥有的视图。
要获取一个连接多个字符串视图的视图,我们可以使用在 C++20 中引入的 `join` 视图适配器。
    auto const joined = std::views::join(std::array{s1, s2});

这给我们一个可以使用标准算法或基于范围的 for 进行迭代的视图对象。它可以转换为一个 std::string 对象(但不能直接转换为 std::string_view,因为那需要将内容复制到某个地方使其连续)。
完整演示:
#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"});
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接