首页 文章

将const std :: string&作为参数传递的日子是多少?

提问于
浏览
531

我听到Herb Sutter最近的一次演讲,他提出通过 std::vectorstd::string 的理由基本上没有了 . 他建议现在更好地编写如下函数:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

我知道 return_val 在函数返回时将是一个右值,因此可以使用非常便宜的移动语义返回 . 但是, inval 仍然远大于引用的大小(通常实现为指针) . 这是因为 std::string 具有各种组件,包括指向堆的指针和用于短字符串优化的成员 char[] . 所以在我看来,通过引用传递仍然是一个好主意 .

任何人都可以解释为什么赫伯可能会说这个吗?

13 回答

  • 359

    赫伯说他说的原因是因为这样的情况 .

    假设我有函数 A ,它调用函数 B ,它调用函数 C . 并且 A 将字符串传递给 B 并进入 C . A 不知道或不关心 C ;所有 A 知道的是 B . 也就是说, CB 的实现细节 .

    假设A的定义如下:

    void A()
    {
      B("value");
    }
    

    如果B和C通过 const& 获取字符串,那么它看起来像这样:

    void B(const std::string &str)
    {
      C(str);
    }
    
    void C(const std::string &str)
    {
      //Do something with `str`. Does not store it.
    }
    

    一切都很好 . 你're just passing pointers around, no copying, no moving, everyone'很高兴 . C 需要 const& ,因为它不存储字符串 . 它只是使用它 .

    现在,我想做一个简单的改变: C 需要将字符串存储在某处 .

    void C(const std::string &str)
    {
      //Do something with `str`.
      m_str = str;
    }
    

    你好,复制构造函数和潜在的内存分配(忽略Short String Optimization (SSO)) . C 11的移动语义应该可以删除不必要的复制构造,对吧? A 临时通过;没有理由为什么 C 必须复制数据 . 它应该只是潜逃而已 .

    除了它不能 . 因为需要 const& .

    如果我更改 C 以按值获取其参数,这只会导致 B 复制到该参数;我一无所获 .

    所以,如果我刚刚通过所有函数传递 str 值,依靠 std::move 来改变数据,我们就不会这样了 .

    它更贵吗?是;移动到一个值比使用引用更昂贵 . 它比副本便宜吗?不适用于SSO的小字符串 . 值得做吗?

    这取决于您的使用案例 . 你讨厌内存分配多少钱?

  • 16

    将const std :: string&作为参数传递的日子是多少?

    No . 许多人将此建议(包括Dave Abrahams)超出其适用的域,并简化它以应用于所有 std::string 参数 - 始终按值传递 std::string 对于任何和所有任意参数和应用程序都不是"best practice"因为优化这些会谈/ articles专注于仅适用于一组受限制的案例 .

    如果您返回一个值,改变参数或取值,那么传递值可以节省昂贵的复制并提供语法上的便利 .

    与以往一样,当您不需要副本时,通过const引用可以节省大量复制 .

    现在到具体的例子:

    然而,inval仍然比引用的大小(通常实现为指针)大得多 . 这是因为std :: string有各种组件,包括指向堆的指针和用于短字符串优化的成员char [] . 所以在我看来,通过引用传递仍然是一个好主意 . 任何人都可以解释为什么赫伯可能会说这个吗?

    如果堆栈大小是一个问题(假设没有内联/优化), return_val inval > return_val - IOW,可以通过在此处传递值来减少峰值堆栈的使用(注意:ABI的过度简化) . 同时,通过const引用可以禁用优化 . 这里的主要原因不是避免堆栈增长,而是确保可以在适用的地方执行优化 .

    通过const引用的日子并没有结束 - 规则比以前更加复杂 . 如果性能很重要,那么根据您在实现中使用的详细信息,最好考虑如何传递这些类型 .

  • 16

    这在很大程度上取决于编译器的实现 .

    但是,它还取决于您使用的是什么 .

    让我们考虑下一个功能:

    bool foo1( const std::string v )
    {
      return v.empty();
    }
    bool foo2( const std::string & v )
    {
      return v.empty();
    }
    

    这些函数在单独的编译单元中实现,以避免内联 . 然后 :
    1.如果你将一个文字传递给这两个函数,你就不会发现性能差异太大 . 在这两种情况下,都必须创建一个字符串对象
    2.如果传递另一个std :: string对象, foo2 将优于 foo1 ,因为 foo1 将执行深层复制 .

    在我的电脑上,使用g 4.6.1,我得到了这些结果:

    • 参考变量:1000000000次迭代 - >经过的时间:2.25912秒

    • 变量值:1000000000次迭代 - >经过的时间:27.2259秒

    • 通过引用的字面值:100000000次迭代 - >经过的时间:9.10319秒

    • 字面值:100000000次迭代 - >经过的时间:8.62659秒

  • 13

    简答: NO! 答案很长:

    • If you won't modify the string (treat is as read-only), pass it as const ref&.
      const ref& 显然需要保持在范围内,而使用它的函数执行)

    • If you plan to modify it or you know it will get out of scope (threads), pass it as a value, don't copy the const ref& inside your function body.

    cpp-next.com 上有一篇名为"Want speed, pass by value!"的帖子 . TL; DR:

    指南:不要复制你的函数参数 . 相反,按值传递它们,让编译器进行复制 .

    TRANSLATION of ^

    Don’t copy your function arguments ---表示:如果您计划通过将参数值复制到内部变量来修改参数值,则只需使用值参数 .

    那么, don't do this

    std::string function(const std::string& aString){
        auto vString(aString);
        vString.clear();
        return vString;
    }
    

    do this

    std::string function(std::string aString){
        aString.clear();
        return aString;
    }
    

    当您需要修改函数体中的参数值时 .

    您只需要知道您打算如何在函数体中使用该参数 . 只读或不...并且如果它在范围内 .

  • 7

    除非你真的需要副本,否则采取 const & 仍然是合理的 . 例如:

    bool isprint(std::string const &s) {
        return all_of(begin(s),end(s),(bool(*)(char))isprint);
    }
    

    如果你改变它以按值获取字符串,那么你将最终移动或复制参数,并且没有必要 . 复制/移动不仅可能更昂贵,而且还会引入新的潜在故障;复制/移动可能会抛出异常(例如,复制期间的分配可能会失败),而对现有值的引用则不能 .

    如果你确实需要一个副本,那么传递和返回值通常(总是?)是最佳选择 . 事实上,我一般不会怀疑你必须检查你的RVO编译器支持表现在已经过时了 .


    简而言之,C 11在这方面并没有真正改变任何东西,除了那些不信任复制品的人 .

  • 43

    几乎 .

    在C17中,我们有 basic_string_view<?> ,这使我们基本上将 std::string const& 参数的一个狭窄用例 .

    移动语义的存在已经消除了 std::string const& 的一个用例 - 如果您计划存储参数,那么采用 std::string by值更为理想,因为您可以在参数之外使用 move .

    如果有人用原始C "string" 调用你的函数,这意味着只分配了一个 std::string 缓冲区,而不是 std::string const& 情况下的两个 .

    但是,如果您不打算复制,则在C14中使用 std::string const& 仍然有用 .

    使用 std::string_view ,只要您没有将所述字符串传递给期望C样式 '\0' -terminated字符缓冲区的API,您就可以更高效地获得类似功能,而不会有任何分配风险 . 原始C字符串甚至可以转换为 std::string_view 而无需任何分配或字符复制 .

    此时, std::string const& 的用途是当你没有复制数据批发时,并且要将它传递给期望空终止缓冲区的C风格的API,并且你需要 std::string 提供的更高级别的字符串函数 . 实际上,这是一组罕见的要求 .

  • 41

    我在这里复制/粘贴了this question的答案,并更改了名称和拼写以适应这个问题 .

    以下是衡量所询问内容的代码:

    #include <iostream>
    
    struct string
    {
        string() {}
        string(const string&) {std::cout << "string(const string&)\n";}
        string& operator=(const string&) {std::cout << "string& operator=(const string&)\n";return *this;}
    #if (__has_feature(cxx_rvalue_references))
        string(string&&) {std::cout << "string(string&&)\n";}
        string& operator=(string&&) {std::cout << "string& operator=(string&&)\n";return *this;}
    #endif
    
    };
    
    #if PROCESS == 1
    
    string
    do_something(string inval)
    {
        // do stuff
        return inval;
    }
    
    #elif PROCESS == 2
    
    string
    do_something(const string& inval)
    {
        string return_val = inval;
        // do stuff
        return return_val; 
    }
    
    #if (__has_feature(cxx_rvalue_references))
    
    string
    do_something(string&& inval)
    {
        // do stuff
        return std::move(inval);
    }
    
    #endif
    
    #endif
    
    string source() {return string();}
    
    int main()
    {
        std::cout << "do_something with lvalue:\n\n";
        string x;
        string t = do_something(x);
    #if (__has_feature(cxx_rvalue_references))
        std::cout << "\ndo_something with xvalue:\n\n";
        string u = do_something(std::move(x));
    #endif
        std::cout << "\ndo_something with prvalue:\n\n";
        string v = do_something(source());
    }
    

    对我来说这个输出:

    $ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=1 test.cpp
    $ a.out
    do_something with lvalue:
    
    string(const string&)
    string(string&&)
    
    do_something with xvalue:
    
    string(string&&)
    string(string&&)
    
    do_something with prvalue:
    
    string(string&&)
    $ clang++ -std=c++11 -stdlib=libc++ -DPROCESS=2 test.cpp
    $ a.out
    do_something with lvalue:
    
    string(const string&)
    
    do_something with xvalue:
    
    string(string&&)
    
    do_something with prvalue:
    
    string(string&&)
    

    下表总结了我的结果(使用clang -std = c 11) . 第一个数字是复制结构的数量,第二个数字是移动结构的数量:

    +----+--------+--------+---------+
    |    | lvalue | xvalue | prvalue |
    +----+--------+--------+---------+
    | p1 |  1/1   |  0/2   |   0/1   |
    +----+--------+--------+---------+
    | p2 |  1/0   |  0/1   |   0/1   |
    +----+--------+--------+---------+
    

    按值传递解决方案只需要一次重载,但在传递左值和x值时需要额外的移动构造 . 对于任何给定的情况,这可能是也可能是不可接受的 . 两种解决方案都有优点和缺点 .

  • 56

    std::string 不是Plain Old Data(POD),它的原始尺寸不是最相关的东西 . 例如,如果传入一个高于SSO长度并在堆上分配的字符串,我希望复制构造函数不复制SSO存储 .

    建议这样做是因为 inval 是从参数表达式构造的,因此总是在适当时移动或复制 - 假设您需要参数的所有权,则不会有性能损失 . 如果不这样做,那么 const 参考仍然是更好的方法 .

  • 130

    除了Bjarne Stroustroup之外,Herb Sutter还在推荐 const std::string& 作为参数类型;见https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in .

    在这里的任何其他答案中都没有提到的缺陷:如果将字符串文字传递给 const std::string& 参数,它将传递对临时字符串的引用,该字符串是即时创建的,用于保存文字的字符 . 如果您随后保存该引用,则在取消分配临时字符串后它将无效 . 为安全起见,您必须保存副本,而不是参考 . 问题源于字符串文字是 const char[N] 类型,需要升级到 std::string .

    下面的代码说明了陷阱和解决方法,以及一个小的效率选项 - 使用 const char* 方法重载,如Is there a way to pass a string literal as reference in C++所述 .

    (注意:Sutter&Stroustroup建议如果你保留一份副本字符串,还提供带有&&参数和std :: move()的重载函数 . )

    #include <string>
    #include <iostream>
    class WidgetBadRef {
    public:
        WidgetBadRef(const std::string& s) : myStrRef(s)  // copy the reference...
        {}
    
        const std::string& myStrRef;    // might be a reference to a temporary (oops!)
    };
    
    class WidgetSafeCopy {
    public:
        WidgetSafeCopy(const std::string& s) : myStrCopy(s)
                // constructor for string references; copy the string
        {std::cout << "const std::string& constructor\n";}
    
        WidgetSafeCopy(const char* cs) : myStrCopy(cs)
                // constructor for string literals (and char arrays);
                // for minor efficiency only;
                // create the std::string directly from the chars
        {std::cout << "const char * constructor\n";}
    
        const std::string myStrCopy;    // save a copy, not a reference!
    };
    
    int main() {
        WidgetBadRef w1("First string");
        WidgetSafeCopy w2("Second string"); // uses the const char* constructor, no temp string
        WidgetSafeCopy w3(w2.myStrCopy);    // uses the String reference constructor
        std::cout << w1.myStrRef << "\n";   // garbage out
        std::cout << w2.myStrCopy << "\n";  // OK
        std::cout << w3.myStrCopy << "\n";  // OK
    }
    

    OUTPUT:

    const char *构造函数
    const std :: string&constructor

    第二串
    第二串

  • 1

    使用 std::string 的C参考的IMO是快速和短的局部优化,而使用传递值可以(或不是)更好的全局优化 .

    所以答案是:它取决于具体情况:

    • 如果您从外部函数编写所有代码到内部函数,您知道代码的作用,您可以使用引用 const std::string & .

    • 如果您编写库代码或使用大量库代码传递字符串,那么通过信任 std::string 复制构造函数行为,您可能在全局意义上获得更多 .

  • 4

    “Herb Sutter "Back to the Basics! Essentials of Modern C++ Style” . 在其他主题中,他回顾了过去给出的参数传递建议,以及C 11中出现的新想法,并特别关注了按值传递字符串的想法 .

    slide 24

    基准测试显示,在函数将以任何方式复制它的情况下,按值传递 std::string s可能会明显变慢!

    这是因为你强制它总是制作一个完整的副本(然后移动到位),而 const& 版本将更新旧的字符串,它可以重用已经分配的缓冲区 .

    请参阅他的幻灯片27:对于“设置”功能,选项1与以往一样 . 选项2为右值参考添加了一个重载,但如果有多个参数,则会产生组合爆炸 .

    它仅适用于必须创建字符串(不更改其现有值)的“接收器”参数,即按值传递技巧有效 . 也就是说, constructors 其中参数直接初始化匹配类型的成员 .

    如果你想看看有多深,你可以担心这个问题,看看Nicolai Josuttis’s演示并祝你好运(“完美 - 完成!”在找到上一版本的错误后n次 . 曾经去过吗?)


    这也在标准指南中总结为⧺F.15 .

  • 2

    正如@JDługosz在评论中指出的那样,Herb在另一个(后来的?)谈话中给出了其他建议,大致从这里看:https://youtu.be/xnqTKD8uD64?t=54m50s .

    他的建议归结为仅使用函数 f 的值参数,该函数采用所谓的接收器参数,假设您将从这些接收器参数移动构造 .

    与仅为lvalue和rvalue参数定制的 f 的最佳实现相比,这种通用方法仅增加了lvalue和rvalue参数的移动构造函数的开销 . 要查看为什么会出现这种情况,假设 f 采用值参数,其中 T 是一些副本并移动可构造类型:

    void f(T x) {
      T y{std::move(x)};
    }
    

    使用左值参数调用 f 将导致调用复制构造函数来构造 x ,并调用移动构造函数来构造 y . 另一方面,使用rvalue参数调用 f 将导致调用移动构造函数来构造 x ,并调用另一个移动构造函数来构造 y .

    通常,对于左值参数, f 的最佳实现如下:

    void f(const T& x) {
      T y{x};
    }
    

    在这种情况下,只调用一个复制构造函数来构造 y . 对于右值参数, f 的最佳实现通常再次如下:

    void f(T&& x) {
      T y{std::move(x)};
    }
    

    在这种情况下,只调用一个移动构造函数来构造 y .

    因此,合理的折衷方案是获取一个值参数,并针对最优实现提供一个额外的移动构造函数调用lvalue或rvalue参数,这也是Herb的演讲中给出的建议 .

    正如@JDługosz在评论中指出的那样,传递值只对从sink参数构造一些对象的函数有意义 . 当你有一个复制其参数的函数 f 时,按值传递方法将比一般的const-by-reference方法有更多的开销 . 保留其参数副本的函数 f 的按值传递方法将具有以下形式:

    void f(T x) {
      T y{...};
      ...
      y = std::move(x);
    }
    

    在这种情况下,有一个lvalue参数的复制结构和移动赋值,以及一个rvalue参数的移动构造和移动赋值 . 左值参数的最佳情况是:

    void f(const T& x) {
      T y{...};
      ...
      y = x;
    }
    

    这归结为一个赋值,它可能比复制构造函数便宜得多,并且可以通过值传递方法所需的移动赋值 . 原因是赋值可能会重用 y 中现有的已分配内存,因此会阻止(de)分配,而复制构造函数通常会分配内存 .

    对于rvalue参数,保留副本的 f 的最佳实现具有以下形式:

    void f(T&& x) {
      T y{...};
      ...
      y = std::move(x);
    }
    

    所以,在这种情况下只有移动分配 . 将rvalue传递给采用const引用的 f 版本只需要赋值而不是移动赋值 . 所以相对而言, f 的版本在这种情况下采用const引用作为一般实现是优选的 .

    因此,一般而言,对于最佳实现,您将需要重载或执行某种完美转发,如演讲中所示 . 缺点是所需的重载数量会发生组合爆炸,具体取决于 f 的参数数量,以防您选择在参数的值类别上重载 . 完美转发的缺点是 f 成为模板函数,这会阻止它变为虚拟,并且如果你想让它100%正确,会导致更复杂的代码(请参阅有关血腥细节的讨论) .

  • 21

    问题是"const"是非粒度限定符 . 通常"const string ref"的意思是"don't modify this string",而不是"don't modify the reference count" . 在C中根本没有办法说出哪些成员是"const" . 他们要么都是,要么都不是 .

    为了解决这个语言问题,STL可以允许你的例子中的"C()"无论如何都要进行移动语义拷贝,并且尽职地忽略"const"关于引用计数(可变) . 只要它是明确的,这将是好的 .

    由于STL没有,我有一个字符串的版本,const_casts <>远离引用计数器(无法在类层次结构中追溯性地创建可变的东西),并且 - 并且 - 看哪 - 你可以自由地将cmstring作为const引用传递,并且整天都在深层功能中复制它们,没有泄漏或问题 .

    由于C在这里没有提供“派生类const粒度”,因此编写一个好的规范并创建一个闪亮的新“const可移动字符串”(cmstring)对象是我见过的最好的解决方案 .

相关问题