我最近听了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[]。所以在我看来,通过引用传递仍然是一个好主意。
谁能解释一下赫伯为什么会这么说?
赫伯说那些话的原因就是因为这样的案子。
假设我有一个函数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的小字符串。值得做吗?
这取决于您的用例。你有多讨厌内存分配?
是传递const std::string &作为参数的日子?
不。许多人采纳了这个建议(包括Dave Abrahams),并将其简化为适用于所有std::string参数——始终按值传递std::string对于任何和所有任意参数和应用程序都不是“最佳实践”,因为这些演讲/文章关注的优化只适用于有限的一组情况。
如果要返回值、改变参数或获取值,那么按值传递可以节省昂贵的复制,并提供语法上的便利。
与以往一样,当您不需要拷贝时,传递const引用可以节省大量复制。
现在来看看具体的例子:
然而,inval仍然比引用(通常实现为指针)的大小大得多。这是因为std::string有各种组件,包括指向堆的指针和用于短字符串优化的成员char[]。所以在我看来,通过引用传递仍然是一个好主意。谁能解释一下赫伯为什么会这么说?
如果考虑到堆栈大小(并且假设这不是内联/优化的),return_val + inval > return_val——IOW,可以通过在这里传递值来降低堆栈使用的峰值(注意:ABIs的过度简化)。同时,通过const引用传递可以禁用优化。这里的主要原因不是为了避免堆栈增长,而是为了确保优化可以在适用的地方执行。
通过const引用传递的日子并没有结束——规则只是比以前更复杂了。如果性能很重要,明智的做法是根据实现中使用的细节考虑如何传递这些类型。
赫伯说那些话的原因就是因为这样的案子。
假设我有一个函数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的小字符串。值得做吗?
这取决于您的用例。你有多讨厌内存分配?
这在很大程度上取决于编译器的实现。
然而,这也取决于你使用什么。
让我们考虑下一个函数:
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秒
几乎。
在c++ 17中,我们有basic_string_view<?>,这基本上把我们带到了std::string的一个狭窄的用例。
move语义的存在消除了std::string const&的一个用例——如果您计划存储参数,按值获取std::string是更优的,因为您可以移出参数。
如果有人用一个原始的C“string”调用你的函数,这意味着只有一个std::string缓冲区被分配,而不是std::string const&case中的两个。
然而,如果你不打算复制,通过std::string const&在c++ 14中仍然是有用的。
使用std::string_view,只要你没有将该字符串传递给一个期望c风格以“\0”结尾的字符缓冲区的API,你就可以更有效地获得类似std::string的功能,而无需承担任何分配风险。一个原始的C字符串甚至可以转换为std::string_view,而不需要任何分配或字符复制。
在这一点上,std::string const&的使用是当您不批量复制数据,并将其传递给一个c风格的API,该API期望一个以null结束的缓冲区,并且您需要std::string提供的高级字符串函数。在实践中,这是一组罕见的需求。
除非你真的需要一个副本,否则使用const &仍然是合理的。例如:
bool isprint(std::string const &s) {
return all_of(begin(s),end(s),(bool(*)(char))isprint);
}
如果你改变它,以按值获取字符串,那么你最终会移动或复制参数,这是没有必要的。复制/移动不仅成本更高,而且还会带来新的潜在失败;复制/移动可能会抛出异常(例如,复制期间的分配可能会失败),而引用现有值则不会。
如果你确实需要一个副本,那么通过值传递和返回通常是(总是?)最好的选择。事实上,在c++ 03中我通常不会担心这个问题,除非你发现额外的副本实际上会导致性能问题。复制省略在现代编译器上似乎相当可靠。我认为人们的怀疑和坚持,你必须检查你的编译器支持RVO的表,现在大部分已经过时了。
简而言之,c++ 11在这方面并没有真正改变任何东西,除了那些不相信复制省略的人。