提问者:小点点

构造函数采用std::string_view vs std::string和move


假设我有一个具有std::string成员的类,我想在它的一个构造函数中获取该成员的值。

一种方法是采用std::string类型的参数,然后使用std::move:

Foo(std::string str) : _str(std::move(str)) {}

据我所知,移动字符串只是复制其内部指针,这意味着它基本上是自由的,因此传递常量char*将与传递常量std::string&;一样有效。

然而,在C++17中,我们得到了std::string_view,它承诺提供廉价的副本。 因此上述内容可以写成:

Foo(std::string_view str) : _str(str.begin(), str.end()) {}

不需要移动或构造临时的std::string,但我认为它实际上只做了与以前相同的事情。

我是不是遗漏了什么? 或者仅仅是一个风格问题,即您是使用std::string_view还是使用std::string与move一起使用?


共2个答案

匿名用户

不需要移动或构造临时的std::string,但我认为它实际上只是有效地做了和以前相同的事情。

这完全取决于用户在调用构造函数时拥有什么。 因此,让我们考虑案例1(std::string)和案例2(std::string_view)。 在这两种情况下,最终结果都是std::string。 还有,这个分析会忽略小字符串优化。

下面是我们可以考虑的一些选项:

>

  • 用户具有字符串文本。

    >

  • 在情况1中,将复制到std::string参数中,然后移动到类中的std::string中。

    在案例2中,将有一个指针和大小的副本,然后是类中std::string中字符的副本。

    在这两种情况下,文本的长度都必须在某个时候通过char_traits::length计算。 如果用户在传递参数之前使用UDL(“some_string”s“some_string”sv)计算参数,则可以避免运行时char_traits::length调用。

    所以在这种情况下,它们基本上是一样的。

    用户有一个std::stringlvalue,他们希望保留该值。

    >

  • 在情况1中,将复制到std::string参数中,然后移动到std::string成员中。

    在案例2中,指针和大小将复制到std::string_view参数中,然后是字符复制到类中的std::string

    在这两种情况下,都不会计算长度,因为std::string知道它的长度。 再说一遍,在这种情况下,它们是一样的。

    用户有一个想要移动到对象中的std::string值。 因此这要么是一个prvalue,要么是一个显式的std::move

    >

  • 在情况1中,将有一个参数的move-construction,然后是成员的move-construction。

    在情况2中,将会有一个指针和大小的副本,后面是一个字符到std::string成员中的副本。

    看到区别了吗? 在情况1中,没有字符被复制; 只有动作。 这是因为用户所拥有的和您的类所需要的是完全相同的。 所以你可以得到最有效的传输。

    在情况2中,必须复制字符,因为string_view参数不知道用户不想保留字符串。 因此,正在调用的string成员的构造函数也不会。

    当您在源和目标类型相同的情况下使用中介进行传输时,您可能会引入低效率。 如果用户具有您实际打算使用的类型,那么从性能角度来说,接口最好直接表示该类型。 如果使用视图中介,那么调用方和被调用方之间的信息和意图可能会丢失。

    string_view是一种通用语类型; 它主要用于在不强制用户使用特定字符串类型的情况下使用字符数组。 对于希望在函数调用之外保留这些字符的用例,通用语类型是次优的,因为要保留它们,唯一能做的就是将它们复制到您自己的字符串中。

    除非必须将std::string(或您使用的任何字符串类型)排除在您的接口之外,或者如果用户不可能直接传递存储字符的类型(例如,您可能存储的是数组),否则应该将其用作参数类型。

    但当然,这都是微优化的地盘。 除非这个类被大量使用,否则差别微不足道。

  • 匿名用户

    让我们考虑一些场景:

    Foo(std::string s) : str_(std::move(s)) {}
    string s1;
    
    Foo("abc");           // A - construct from string literal
    Foo (s1);             // B - construct from existing string
    Foo (string("def"));  // C - construct from temporary string
    
    • 在情况(A)中,编译器创建一个临时字符串,将其传递给foo的构造函数,该构造函数从该字符串中移动。
    • 在情况(B)中,编译器制作s1的副本,并将其传递给foo的构造函数,后者从副本中移动。
    • 在情况(C)中,编译器将临时字符串传递给foo的构造函数,该构造函数从该字符串中移出。

    如果相反,我们有:

    Foo(std::string_view sv) : str_(sv.begin() sv.end()) {}
    string s1;
    
    Foo("abc");           // A - construct from string literal
    Foo (s1);             // B - construct from existing string
    Foo (string("def"));  // C - construct from temporary string
    
      在情况(A)中,编译器创建一个string_view(调用strlen)并传递它。 字符数据被复制到str_
    • 在情况(B)中,编译器从s1.data()s1.size()创建一个string_view,并传递它。 字符数据被复制到str_
    • 中 在情况(C)中,
    • 从临时字符串创建一个字符串视图,并传递该视图。 字符数据被复制到str_

    这两种方法在所有情况下都不是最好的。 第一种方法对(A)和(C)非常有效,对(B)也可以

    第二种方法对(A)和(B)非常有效,而对(C)则不那么有效。