何时应该使用 std::string / std::string_view 作为参数/返回类型?

7

介绍

我正在编写一些通信应用程序。在 C++17(没有 Boost)之前,我使用 std::string 及其 const 引用作为 cls1

C++17 引入了 std::string_view 作为 cls2 的一部分。然而,我并没有清晰的政策来决定何时应该使用 std::string_view。我的通信应用程序从网络中接收数据并将其存储到 recv_buffer 中,并从 recv_buffer 创建一些应用程序类。

构造函数

如果只关注 cls1 的构造函数,则移动构造是有效的。但是我认为参数 s 来自哪里很重要。如果它最初来自于 recv_buffer,则可以在接收(非常早期)点创建 std::string_view。并且在 recv_buffer 存在期间,到处使用 std::string_view。如果需要存储 recv_buffer 的一部分,则创建 std::string

我发现唯一的例外是 recv_buffer 总是包含我的应用程序类的完整数据。在这种情况下,移动构造是有效的。

Getter

我认为将返回类型设置为 std::string_view 有优势。一些成员函数如 substr() 是有效的。但是,到目前为止,我没有看到任何缺点。

问题

我怀疑我可能只看到了使用 std::string_view 的优点。在重新编写许多代码之前,我想知道您的想法。

PoC 代码

#include <string>

struct cls1 {
    explicit cls1(std::string s):s_(std::move(s)) {}
    std::string const& get() const { return s_; }
private:
    std::string s_;
};

struct cls2 {
    explicit cls2(std::string_view s):s_(s) {}
    std::string_view get() const { return s_; }
private:
    std::string s_;
};

#include <iostream>

int main() {
    // If all of the receive buffer is the target
    {
        std::string recv_buffer = "ABC";
        cls1 c1(std::move(recv_buffer)); // move construct
        std::cout << c1.get().substr(1, 2) << std::endl; // create new string
    }
    {
        std::string recv_buffer = "ABC";
        cls2 c2(recv_buffer);            // copy happend
        std::cout << c2.get().substr(1, 2) << std::endl; // doesn't create new string
    }

    // If a part of the receive buffer is the target
    {
        std::string recv_buffer = "<<<ABC>>>";
        cls1 c1(recv_buffer.substr(3, 3)); // copy happend and move construct
        std::cout << c1.get().substr(1, 2) << std::endl; // create new string
    }
    {
        std::string recv_buffer = "<<<ABC>>>";
        std::string_view ref = recv_buffer;
        cls2 c2(ref.substr(3, 3)); // string create from the part of buffer directly
        std::cout << c2.get().substr(1, 2) << std::endl; // doesn't create new string
    }
}

运行演示:https://wandbox.org/permlink/TW8w3je3q3D46cjk


2
有人几天前问了非常类似的问题:https://dev59.com/DFMI5IYBdhLWcg3wRZbJ - R2RT
2
考虑将string_view视为引用。如果它所引用的对象消失了,那么就会出现问题。 - Michael Chourdakis
此外,std::string_view缺少所有修改器方法std::string具有(当使用const引用时您不需要关心),同样缺少 std::string::c_str() 方法。 - YSC
2个回答

13

std::string_view是一种获取std::string某些const成员函数的方式,而不需要创建一个std::string,如果你有一些char*或者想要引用字符串的子集,请考虑它作为一个const引用。如果它所引用的对象因任何原因消失(或更改),你就会遇到问题。如果你的代码能够返回引用,那么你可以返回一个string_view。

例如:

#include <cstdio>
#include <string>
#include <vector>
#include <string.h>
#include <iostream>

int main()
{
    char* a = new char[10];
    strcpy(a,"Hello");
    std::string_view s(a);
    std::cout << s; // OK    
    delete[] a;
    std::cout << s;     // whops. UD. If it was std::string, no problem, it would have been a copy
}

更多信息

编辑:它没有c_str()成员,因为这需要在子字符串末尾创建一个\0,而这无法在不修改的情况下完成。


谢谢!我理解了生命周期的概念。它类似于 std::string const&std::string_view。如果两者都可以,而且我不需要空终止符,我会选择 std::string_view。我决定这是我的策略。唯一的例外是完整的 std::string 可以作为构造函数的参数。在这种情况下,我将使用 std::stringstd::string&& 作为构造函数的参数。 - Takatoshi Kondo
我认为只有在处理char*时,我才会使用string_view。如果已经有std::string,则只有在需要子字符串时才将其转换为string_view。 - Michael Chourdakis
我的问题在最初提问时比较笼统,但实际上 recv_buffer 是 MQTT 数据包,例如 https://docs.oasis-open.org/mqtt/mqtt/v5.0/os/mqtt-v5.0-os.html#_Table_3.4_。在这种情况下,我认为使用基于 std::string_view 的方法更好,以从 recv_buffer 中获取字符串 a/bc/d。正如您所提到的那样,这是一种子字符串情况。 - Takatoshi Kondo

6

当需要以下情况时,请不要返回字符串视图:

  • 调用者需要一个以 null 结尾的字符串。在处理 C API 时经常会遇到这种情况。
  • 您没有在某处存储字符串本身。在这种情况下,您将字符串存储在成员中。

请注意,原始字符串上的操作(例如更改容量)以及原始字符串被销毁都会使字符串视图失效。如果调用者需要在存储字符串的对象的生命周期之外使用该字符串,则可以从视图中复制到自己的存储器中。


谢谢!我理解std::string_view的缺点。第一点是合理的。我明白第二点的意思是“不要返回局部临时对象的引用”。在这种情况下,返回类型是std::stringstd::string_viewstd::string const&都不好。是这样吗? - Takatoshi Kondo
2
@TakatoshiKondo 返回字符串视图或局部字符串的引用都不好。 - eerorika

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