我听到Herb Sutter最近的一次演讲,他提出通过 std::vector
和 std::string
的理由基本上没有了 . 他建议现在更好地编写如下函数:
std::string do_something ( std::string inval )
{
std::string return_val;
// ... do stuff ...
return return_val;
}
我知道 return_val
在函数返回时将是一个右值,因此可以使用非常便宜的移动语义返回 . 但是, inval
仍然远大于引用的大小(通常实现为指针) . 这是因为 std::string
具有各种组件,包括指向堆的指针和用于短字符串优化的成员 char[]
. 所以在我看来,通过引用传递仍然是一个好主意 .
任何人都可以解释为什么赫伯可能会说这个吗?
13 回答
赫伯说他说的原因是因为这样的情况 .
假设我有函数
A
,它调用函数B
,它调用函数C
. 并且A
将字符串传递给B
并进入C
.A
不知道或不关心C
;所有A
知道的是B
. 也就是说,C
是B
的实现细节 .假设A的定义如下:
如果B和C通过
const&
获取字符串,那么它看起来像这样:一切都很好 . 你're just passing pointers around, no copying, no moving, everyone'很高兴 .
C
需要const&
,因为它不存储字符串 . 它只是使用它 .现在,我想做一个简单的改变:
C
需要将字符串存储在某处 .你好,复制构造函数和潜在的内存分配(忽略Short String Optimization (SSO)) . C 11的移动语义应该可以删除不必要的复制构造,对吧?
A
临时通过;没有理由为什么C
必须复制数据 . 它应该只是潜逃而已 .除了它不能 . 因为需要
const&
.如果我更改
C
以按值获取其参数,这只会导致B
复制到该参数;我一无所获 .所以,如果我刚刚通过所有函数传递
str
值,依靠std::move
来改变数据,我们就不会这样了 .它更贵吗?是;移动到一个值比使用引用更昂贵 . 它比副本便宜吗?不适用于SSO的小字符串 . 值得做吗?
这取决于您的使用案例 . 你讨厌内存分配多少钱?
No . 许多人将此建议(包括Dave Abrahams)超出其适用的域,并简化它以应用于所有
std::string
参数 - 始终按值传递std::string
对于任何和所有任意参数和应用程序都不是"best practice"因为优化这些会谈/ articles专注于仅适用于一组受限制的案例 .如果您返回一个值,改变参数或取值,那么传递值可以节省昂贵的复制并提供语法上的便利 .
与以往一样,当您不需要副本时,通过const引用可以节省大量复制 .
现在到具体的例子:
如果堆栈大小是一个问题(假设没有内联/优化),
return_val
inval
>return_val
- IOW,可以通过在此处传递值来减少峰值堆栈的使用(注意:ABI的过度简化) . 同时,通过const引用可以禁用优化 . 这里的主要原因不是避免堆栈增长,而是确保可以在适用的地方执行优化 .通过const引用的日子并没有结束 - 规则比以前更加复杂 . 如果性能很重要,那么根据您在实现中使用的详细信息,最好考虑如何传递这些类型 .
这在很大程度上取决于编译器的实现 .
但是,它还取决于您使用的是什么 .
让我们考虑下一个功能:
这些函数在单独的编译单元中实现,以避免内联 . 然后 :
1.如果你将一个文字传递给这两个函数,你就不会发现性能差异太大 . 在这两种情况下,都必须创建一个字符串对象
2.如果传递另一个std :: string对象,
foo2
将优于foo1
,因为foo1
将执行深层复制 .在我的电脑上,使用g 4.6.1,我得到了这些结果:
参考变量:1000000000次迭代 - >经过的时间:2.25912秒
变量值:1000000000次迭代 - >经过的时间:27.2259秒
通过引用的字面值:100000000次迭代 - >经过的时间:9.10319秒
字面值:100000000次迭代 - >经过的时间:8.62659秒
简答: 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 :
do this :
当您需要修改函数体中的参数值时 .
您只需要知道您打算如何在函数体中使用该参数 . 只读或不...并且如果它在范围内 .
除非你真的需要副本,否则采取
const &
仍然是合理的 . 例如:如果你改变它以按值获取字符串,那么你将最终移动或复制参数,并且没有必要 . 复制/移动不仅可能更昂贵,而且还会引入新的潜在故障;复制/移动可能会抛出异常(例如,复制期间的分配可能会失败),而对现有值的引用则不能 .
如果你确实需要一个副本,那么传递和返回值通常(总是?)是最佳选择 . 事实上,我一般不会怀疑你必须检查你的RVO编译器支持表现在已经过时了 .
简而言之,C 11在这方面并没有真正改变任何东西,除了那些不信任复制品的人 .
几乎 .
在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
提供的更高级别的字符串函数 . 实际上,这是一组罕见的要求 .我在这里复制/粘贴了this question的答案,并更改了名称和拼写以适应这个问题 .
以下是衡量所询问内容的代码:
对我来说这个输出:
下表总结了我的结果(使用clang -std = c 11) . 第一个数字是复制结构的数量,第二个数字是移动结构的数量:
按值传递解决方案只需要一次重载,但在传递左值和x值时需要额外的移动构造 . 对于任何给定的情况,这可能是也可能是不可接受的 . 两种解决方案都有优点和缺点 .
std::string
不是Plain Old Data(POD),它的原始尺寸不是最相关的东西 . 例如,如果传入一个高于SSO长度并在堆上分配的字符串,我希望复制构造函数不复制SSO存储 .建议这样做是因为
inval
是从参数表达式构造的,因此总是在适当时移动或复制 - 假设您需要参数的所有权,则不会有性能损失 . 如果不这样做,那么const
参考仍然是更好的方法 .除了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()的重载函数 . )
OUTPUT:
第二串
第二串
使用
std::string
的C参考的IMO是快速和短的局部优化,而使用传递值可以(或不是)更好的全局优化 .所以答案是:它取决于具体情况:
如果您从外部函数编写所有代码到内部函数,您知道代码的作用,您可以使用引用
const std::string &
.如果您编写库代码或使用大量库代码传递字符串,那么通过信任
std::string
复制构造函数行为,您可能在全局意义上获得更多 .见“Herb Sutter "Back to the Basics! Essentials of Modern C++ Style” . 在其他主题中,他回顾了过去给出的参数传递建议,以及C 11中出现的新想法,并特别关注了按值传递字符串的想法 .
基准测试显示,在函数将以任何方式复制它的情况下,按值传递
std::string
s可能会明显变慢!这是因为你强制它总是制作一个完整的副本(然后移动到位),而
const&
版本将更新旧的字符串,它可以重用已经分配的缓冲区 .请参阅他的幻灯片27:对于“设置”功能,选项1与以往一样 . 选项2为右值参考添加了一个重载,但如果有多个参数,则会产生组合爆炸 .
它仅适用于必须创建字符串(不更改其现有值)的“接收器”参数,即按值传递技巧有效 . 也就是说, constructors 其中参数直接初始化匹配类型的成员 .
如果你想看看有多深,你可以担心这个问题,看看Nicolai Josuttis’s演示并祝你好运(“完美 - 完成!”在找到上一版本的错误后n次 . 曾经去过吗?)
这也在标准指南中总结为⧺F.15 .
正如@JDługosz在评论中指出的那样,Herb在另一个(后来的?)谈话中给出了其他建议,大致从这里看:https://youtu.be/xnqTKD8uD64?t=54m50s .
他的建议归结为仅使用函数
f
的值参数,该函数采用所谓的接收器参数,假设您将从这些接收器参数移动构造 .与仅为lvalue和rvalue参数定制的
f
的最佳实现相比,这种通用方法仅增加了lvalue和rvalue参数的移动构造函数的开销 . 要查看为什么会出现这种情况,假设f
采用值参数,其中T
是一些副本并移动可构造类型:使用左值参数调用
f
将导致调用复制构造函数来构造x
,并调用移动构造函数来构造y
. 另一方面,使用rvalue参数调用f
将导致调用移动构造函数来构造x
,并调用另一个移动构造函数来构造y
.通常,对于左值参数,
f
的最佳实现如下:在这种情况下,只调用一个复制构造函数来构造
y
. 对于右值参数,f
的最佳实现通常再次如下:在这种情况下,只调用一个移动构造函数来构造
y
.因此,合理的折衷方案是获取一个值参数,并针对最优实现提供一个额外的移动构造函数调用lvalue或rvalue参数,这也是Herb的演讲中给出的建议 .
正如@JDługosz在评论中指出的那样,传递值只对从sink参数构造一些对象的函数有意义 . 当你有一个复制其参数的函数
f
时,按值传递方法将比一般的const-by-reference方法有更多的开销 . 保留其参数副本的函数f
的按值传递方法将具有以下形式:在这种情况下,有一个lvalue参数的复制结构和移动赋值,以及一个rvalue参数的移动构造和移动赋值 . 左值参数的最佳情况是:
这归结为一个赋值,它可能比复制构造函数便宜得多,并且可以通过值传递方法所需的移动赋值 . 原因是赋值可能会重用
y
中现有的已分配内存,因此会阻止(de)分配,而复制构造函数通常会分配内存 .对于rvalue参数,保留副本的
f
的最佳实现具有以下形式:所以,在这种情况下只有移动分配 . 将rvalue传递给采用const引用的
f
版本只需要赋值而不是移动赋值 . 所以相对而言,f
的版本在这种情况下采用const引用作为一般实现是优选的 .因此,一般而言,对于最佳实现,您将需要重载或执行某种完美转发,如演讲中所示 . 缺点是所需的重载数量会发生组合爆炸,具体取决于
f
的参数数量,以防您选择在参数的值类别上重载 . 完美转发的缺点是f
成为模板函数,这会阻止它变为虚拟,并且如果你想让它100%正确,会导致更复杂的代码(请参阅有关血腥细节的讨论) .问题是"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)对象是我见过的最好的解决方案 .