我最近听了Herb Sutter的一个演讲,他认为通过const &传递std::vector和std::string的原因已经基本消失了。他建议现在最好编写如下这样的函数:

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

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

谁能解释一下赫伯为什么会这么说?


当前回答

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

然而,这也取决于你使用什么。

让我们考虑下一个函数:

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将进行深度复制。

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

参考变量:1000000000次迭代——>时间流逝:2.25912秒 变量值:1000000000次迭代—>时间流逝:27.2259秒 参考文字:100000000次迭代——>时间流逝:9.10319秒 字面值:100000000次迭代——>时间流逝:8.62659秒

其他回答

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

下面是用来衡量问题的代码:

#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值时需要额外的move构造。对于任何特定的情况,这可能是可接受的,也可能是不可接受的。这两种解决方案各有优缺点。

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

然而,这也取决于你使用什么。

让我们考虑下一个函数:

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将进行深度复制。

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

参考变量:1000000000次迭代——>时间流逝:2.25912秒 变量值:1000000000次迭代—>时间流逝:27.2259秒 参考文字:100000000次迭代——>时间流逝:9.10319秒 字面值:100000000次迭代——>时间流逝:8.62659秒

除非你真的需要一个副本,否则使用const &仍然是合理的。例如:

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

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

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


简而言之,c++ 11在这方面并没有真正改变任何东西,除了那些不相信复制省略的人。

简单的回答:不!长一点的回答:

如果不修改字符串(treat是只读的),则将其作为const ref&传递。(const ref&显然需要在使用它的函数执行时保持在作用域内) 如果你打算修改它,或者你知道它将超出范围(线程),将它作为一个值传递,不要在函数体中复制const ref&。

在cpp-next.com网站上有一篇文章叫做“想要速度,而不是价值!”TL;博士:

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

^的翻译

不要复制你的函数实参——意思是:如果你打算通过将实参复制到内部变量来修改实参值,只需使用一个值实参即可。

所以,不要这样做:

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

这样做:

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

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

您只需要注意计划如何在函数体中使用参数。只读或非只读…如果它在范围内。

赫伯说那些话的原因就是因为这样的案子。

假设我有一个函数A,它调用函数B,函数B调用函数C, A将一个字符串通过B传递给C, A不知道也不关心C;A只知道B,也就是说,C是B的一个实现细节。

假设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接受一个参数&,因为它不存储字符串。它只是简单地使用它。

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

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

你好,复制构造函数和潜在的内存分配(忽略短字符串优化(SSO))。c++ 11的move语义应该可以消除不必要的复制构造,对吧?A只是暂时的;C没有理由复制数据。它应该带着给它的东西潜逃。

但它不能。因为它需要一个常量。

如果我改变C的参数值,这只会导致B对参数进行复制;我什么也得不到。

所以如果我在所有函数中都按值传递str,依靠std::move来打乱数据,我们就不会有这个问题。如果有人想留住它,他们可以做到。如果没有,那好吧。

会更贵吗?是的,移动到值中比使用引用代价更大。它比复制品便宜吗?不适合使用SSO的小字符串。值得做吗?

这取决于您的用例。你有多讨厌内存分配?