652

我听过 Herb Sutter 最近的一次演讲,他认为过去std::vector和过去std::string的理由const &基本上已经不复存在了。他建议现在最好编写如下函数:

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

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

谁能解释为什么 Herb 会这么说?

4

13 回答 13

422

赫布之所以这么说,就是因为这样的案例。

假设我有调用函数的A函数B,它调用函数C。并将A一个字符串传递BC. A不知道也不关心C;众所周知A的是B。也就是说,C是 的实现细节B

假设 A 定义如下:

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

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

void B(const std::string &str)
{
  C(str);
}

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

一切都很好。你只是在传递指针,没有复制,没有移动,每个人都很开心。C需要 aconst&因为它不存储字符串。它只是使用它。

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

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

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

除非它不能。因为它需要一个const&.

如果我更改C为按值获取其参数,那只会导致B复制到该参数中;我一无所获。

因此,如果我只是str通过所有函数按值传递,依靠std::moveshuffle 数据,我们就不会有这个问题。如果有人想坚持下去,他们可以。如果他们不这样做,哦,好吧。

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

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

于 2012-04-19T16:41:47.903 回答
174

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

没有。许多人将这个建议(包括 Dave Abrahams)超越了它所适用的领域,并将其简化为适用于所有 std::string参数——对于任何和所有任意参数和应用程序,始终按值传递std::string并不是“最佳实践”,因为这些优化会谈/文章侧重于仅适用于一组有限的案例

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

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

现在到具体的例子:

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

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

通过 const 引用传递的日子还没有结束——规则比以前更复杂了。如果性能很重要,明智的做法是根据您在实现中使用的详细信息来考虑如何传递这些类型。

于 2012-04-19T21:07:49.760 回答
67

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

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

让我们考虑下一个功能:

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 秒
于 2012-04-19T15:44:04.517 回答
64

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

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

cpp-next.com上有一篇帖子叫“要速度,要价值传递!” . TL;DR:

准则:不要复制你的函数参数。相反,按值传递它们并让编译器进行复制。

^ 的翻译

不要复制函数参数--- 意味着:如果您打算通过将参数值复制到内部变量来修改参数值,则只需使用值参数即可

所以,不要这样做

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

这样做

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

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

你只需要知道你打算如何在函数体中使用参数。只读或非...以及它是否在范围内。

于 2013-08-23T16:33:00.330 回答
44

除非您确实需要副本,否则仍然可以合理使用const &. 例如:

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

如果您将其更改为按值获取字符串,那么您最终将移动或复制参数,而无需这样做。不仅复制/移动可能更昂贵,而且还引入了新的潜在故障;复制/移动可能会引发异常(例如,复制期间的分配可能会失败),而引用现有值则不能。

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


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

于 2012-04-19T15:29:34.680 回答
31

几乎。

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

移动语义的存在已经消除了一个用例std::string const&——如果您计划存储参数,则按std::string值取值更佳,因为您可以move将参数排除在外。

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

但是,如果您不打算制作副本,那么std::string const&在 C++14 中采用仍然很有用。

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

在这一点上,std::string const&当您不批量复制数据并将其传递给需要空终止缓冲区的 C 样式 API 时,使用 for 时,您需要std::string提供的更高级别的字符串函数。在实践中,这是一组罕见的要求。

于 2015-01-26T03:09:35.937 回答
18

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

推荐这样做的原因是因为inval它是根据参数表达式构造的,因此总是根据需要移动或复制 - 假设您需要参数的所有权,不会造成性能损失。如果您不这样做,const参考仍然可能是更好的方法。

于 2012-04-19T15:24:01.737 回答
17

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

这是衡量所问内容的代码:

#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   |
+----+--------+--------+---------+

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

于 2012-04-19T16:13:27.920 回答
15

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 & 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
}

输出:

const char * constructor
const std::string& constructor

Second string
Second string
于 2016-01-27T15:50:57.590 回答
11
于 2018-04-25T18:32:12.863 回答
8

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

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

  1. 如果你把从外到内的所有代码都写在函数里,你就知道代码做了什么,你可以使用参考const std::string &
  2. 如果您编写库代码或在传递字符串的地方大量使用库代码,您可能会通过信任std::string复制构造函数行为获得更多全局意义。
于 2012-04-25T09:26:34.833 回答
3

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

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

f与分别针对左值和右值参数定制的最佳实现相比,这种通用方法仅增加了左值和右值参数的移动构造函数的开销。要了解为什么会出现这种情况,假设f需要一个值参数,其中T是一些复制和移动可构造类型:

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

使用左值参数调用f将导致调用构造函数的复制构造函数x和调用构造函数的移动构造函数y。另一方面,f使用右值参数调用会导致调用构造函数x移动构造函数,调用构造函数调用另一个移动构造函数y

一般来说,f左值参数的最佳实现如下:

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

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

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

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

因此,一个明智的折衷方案是采用一个值参数,并针对最佳实现对左值或右值参数进行一个额外的移动构造函数调用,这也是 Herb 演讲中给出的建议。

正如@JDługosz 在评论中指出的那样,按值传递仅对将从 sink 参数构造某个对象的函数有意义。当您有一个f复制其参数的函数时,按值传递方法将比一般的按常量引用方法具有更多开销。f保留其参数副本的函数的按值传递方法将具有以下形式:

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

在这种情况下,左值参数有复制构造和移动赋值,右值参数有移动构造和移动赋值。左值参数的最佳情况是:

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

这仅归结为一个赋值,它可能比复制构造函数加上传递值方法所需的移动赋值便宜得多。这样做的原因是分配可能会重用 中现有的分配内存y,因此会阻止(取消)分配,而复制构造函数通常会分配内存。

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

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

因此,在这种情况下只有一个移动分配。将右值传递给f接受 const 引用的版本只需要赋值而不是移动赋值。所以相对来说,f在这种情况下采用 const 引用作为通用实现的版本是更可取的。

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

于 2017-10-12T15:41:52.403 回答
0

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

为了解决这个语言问题,STL可以允许您的示例中的“C()”无论如何都制作移动语义副本,并尽职尽责地忽略与引用计数(可变)有关的“const”。只要指定得当,就可以了。

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

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

于 2015-02-20T14:36:49.897 回答