是传递const std :: string&的日子吗?作为一个参数?

时间:2012-04-19 15:20:58

标签: c++ c++11

我听到Herb Sutter最近的一次演讲,他建议std::vectorstd::string通过const &的原因基本消失了。他建议现在更好地编写如下函数:

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

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

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

13 个答案:

答案 0 :(得分:373)

Herb说他说的原因是因为这样的情况。

假设我有函数A,它调用函数B,调用函数CA将字符串传递到B并传递到CA不了解或关心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.
}

一切都很好。你只是传递指针,没有复制,没有移动,每个人都很开心。 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的小字符串。值得做吗?

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

答案 1 :(得分:142)

  

传递const std :: string&的日子吗?作为参数?

即可。许多人将此建议(包括Dave Abrahams)超出其适用的域名,并将其简化为适用于所有 std::string参数 - 始终传递{{ 1}} by value不是任何和所有任意参数和应用程序的“最佳实践”,因为这些讲座/文章所关注的优化只将应用于一组有限的案例

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

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

现在来看具体的例子:

  

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

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

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

答案 2 :(得分:59)

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

但是,这也取决于你使用的是什么。

让我们考虑下一个功能:

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

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

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

  • 通过引用变量:1000000000次迭代 - >经过的时间:2.25912秒
  • 按值变量:1000000000次迭代 - >时间流逝:27.2259秒
  • 通过引用的文字:100000000次迭代 - >时间流逝:9.10319秒
  • 按值计算:100000000次迭代 - >时间流逝:8.62659秒

答案 3 :(得分:47)

简短回答:不!答案很长:

  • 如果您不修改字符串(处理为只读),请将其作为const ref&传递。
    const ref&显然需要保持在范围内,而使用它的函数执行)
  • 如果您打算修改它,或者您知道它将超出范围(线程),请将其作为value传递,不要复制{{1在你的函数体内。

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

  

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

翻译^

不要复制函数参数 ---表示:如果计划通过将参数值复制到内部变量来修改参数值,只需使用值参数

所以,不要这样做

const ref&

执行此操作

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

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

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

答案 4 :(得分:43)

除非您确实需要副本,否则采用const &仍然是合理的。例如:

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

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

如果你需要一份副本,那么按值传递和返回通常(总是?)是最佳选择。事实上,我通常不会在C ++ 03中担心它,除非你发现额外的副本实际上会导致性能问题。在现代编译器上,复制省略似乎相当可靠。我认为人们怀疑和坚持你必须检查你的RVO编译器支持表现在已经过时了。


简而言之,除了那些不信任copy elision的人之外,C ++ 11在这方面并没有真正改变任何东西。

答案 5 :(得分:22)

几乎。

在C ++ 17中,我们有basic_string_view<?>,这使我们基本上了解了std::string const&个参数的一个狭窄用例。

移动语义的存在已经消除了std::string const&的一个用例 - 如果您计划存储参数,那么按std::string取值会更加优化,因为{{1}超出参数。

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

但是,如果您不打算复制,则std::string const&的使用在C ++ 14中仍然有用。

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

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

答案 6 :(得分:16)

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

推荐这个的原因是因为inval是从参数表达式构造的,因此总是在适当时移动或复制 - 假设你需要参数的所有权,就没有性能损失。如果不这样做,const参考仍然是更好的方法。

答案 7 :(得分:16)

我在这里复制/粘贴了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   |
+----+--------+--------+---------+

pass-by-value解决方案只需要一次重载,但在传递lvalues和xvalues时会花费额外的移动构造。对于任何给定的情况,这可能是也可能是不可接受的。两种解决方案都有优点和缺点。

答案 8 :(得分:13)

Herb Sutter和Bjarne Stroustroup一起推荐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&amp; Stroustroup建议如果你保留一个字符串的副本,也提供带有&amp;&amp;参数和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
}

输出:

const char * constructor
const std::string& constructor

Second string
Second string

答案 9 :(得分:7)

使用std::string的C ++参考的IMO是一种快速而简短的本地优化,而使用传递值可能(或不是)更好的全局优化。

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

  1. 如果您编写从外部到内部函数的所有代码,您知道代码的作用,您可以使用引用const std::string &
  2. 如果您编写库代码或使用大量库代码传递字符串,那么通过信任std::string复制构造函数行为,您可能在全局意义上获得更多。

答案 10 :(得分:5)

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

slide 24

基准测试表明,在函数将其复制的情况下,按值传递std::string可能会明显变慢!

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

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

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

如果你想看看你有多担心这个问题,请观看Nicolai Josuttis’s演示并祝你好运(“完美 - 完成!”找出错误后n次以前的版本。曾经去过那里吗?)

标准指南中也将其归纳为⧺F.15

答案 11 :(得分:2)

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

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

这种通用方法只为lvalue和rvalue参数添加了移动构造函数的开销,而不是分别为lvalue和rvalue参数定制的f的最佳实现。要查看原因,请假设f采用值参数,其中T是一些副本并移动可构造类型:

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

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

通常,lvalue参数的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%正确,会导致更复杂的代码(参见有关血腥细节的讨论)。 p>

答案 12 :(得分:1)

问题是“const”是非粒度限定符。 “const string ref”通常的含义是“不要修改此字符串”,而不是“不要修改引用计数”。在C ++中,根本没办法说哪些成员是“const”。他们要么都是,要么都不是。

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

由于STL没有,我有一个const_casts&lt;&gt;字符串的版本离开引用计数器(没有办法在类层次结构中追溯可变的东西),并且 - 看哪 - 你可以自由地将cmstring作为const引用传递,并在深层函数中复制它们,整天都没有泄漏或的问题。

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