我最近听了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[]。所以在我看来,通过引用传递仍然是一个好主意。

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


当前回答

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

他的建议可以归结为,对于一个接受所谓汇聚参数的函数f,只使用值形参,假设您将从这些汇聚参数中移动construct。

与分别为左值和右值参数定制的f的最佳实现相比,这种通用方法只是同时为左值和右值参数增加了move构造函数的开销。要了解为什么会出现这种情况,假设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)};
}

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

因此,一个明智的妥协是,取一个value形参,并有一个额外的move构造函数调用,用于左值或右值参数,这也是Herb在演讲中给出的建议。

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

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

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

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

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

对于右值实参,保留副本的f的最优实现形式为:

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

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

So in general, for the most optimal implementation, you will need to overload or do some kind of perfect forwarding as shown in the talk. The drawback is a combinatorial explosion in the number of overloads required, depending on the number of parameters for f in case you opt to overload on the value category of the argument. Perfect forwarding has the drawback that f becomes a template function, which prevents making it virtual, and results in significantly more complex code if you want to get it 100% right (see the talk for the gory details).

其他回答

string不是普通旧数据(POD),它的原始大小不是最相关的东西。例如,如果传入的字符串超过SSO的长度,并且分配在堆上,我希望复制构造函数不复制SSO存储。

推荐这样做的原因是,inval是从参数表达式构造的,因此总是被适当地移动或复制——假设您需要参数的所有权,这不会造成性能损失。如果不这样做,const引用仍然是更好的方法。

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

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

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

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


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

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

为了解决这个语言问题,STL可以允许“C()”在你的例子中做一个移动语义复制,并在引用计数(可变)方面尽责地忽略“const”。只要它是指定好的,这就可以了。

因为STL没有,我有一个const_cast <>的字符串版本,去掉引用计数器(没有办法在类层次结构中追溯一些可变的东西),并且-你瞧-你可以自由地传递cmstring作为const引用,并在深层函数中复制它们,一整天,没有泄漏或问题。

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

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

不。许多人采纳了这个建议(包括Dave Abrahams),并将其简化为适用于所有std::string参数——始终按值传递std::string对于任何和所有任意参数和应用程序都不是“最佳实践”,因为这些演讲/文章关注的优化只适用于有限的一组情况。

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

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

现在来看看具体的例子:

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

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

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

参见“Herb Sutter”回归基础!《现代c++风格要点》。在其他主题中,他回顾了过去给出的参数传递建议,以及c++ 11中引入的新思想,并特别介绍了按值传递字符串的思想。

基准测试显示,在函数无论如何都会复制std::strings的情况下,按值传递std::strings可能会显著变慢!

这是因为你强迫它总是做一个完整的拷贝(然后移动到适当的位置),而const&version将更新旧字符串,这可能会重用已经分配的缓冲区。

请看他的幻灯片27:对于“set”函数,选项1一如既往。选项2为右值引用添加了重载,但如果有多个参数,则会导致组合爆炸。

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

如果你想知道你能有多担心这个问题,看看尼科莱·约苏蒂斯的演讲,祝你好运(“完美-完成!”n次后发现错误的前一个版本。你去过那里吗?)


这也总结为⧺F。标准指南中的15条。


更新

一般情况下,你需要声明"string"参数为std::string_view(按值)。这允许你像使用const std::string&一样有效地传递一个现有的std::string对象,也可以传递一个词汇字符串字面量(如“hello!”)而不复制它,并传递类型为string_view的对象,现在这些也在生态系统中是必要的。

例外情况是当函数需要一个实际的std::string实例,以便传递给另一个声明为接受const std::string&的函数。