const std::string &作为参数结束?

我听过Herb Sutter最近的一次演讲,他认为通过std::vectorstd::string const &的理由已经基本消失了。他建议现在最好编写如下这样的函数:

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

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

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

245553 次浏览

std::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在这方面并没有真正改变任何东西,除了那些不相信复制省略的人。

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

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

让我们考虑下一个函数:

bool foo1( const std::string v )
{
return v.empty();
}
bool foo2( const std::string & v )
{
return v.empty();
}
为了避免内联,这些函数在单独的编译单元中实现。然后:< br > 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构造。对于任何特定的情况,这可能是可接受的,也可能是不可接受的。这两种解决方案各有优缺点。

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

假设函数A调用函数B,函数CA将一个字符串从B传递到CA不知道也不关心C;A只知道B。也就是说,CB的实现细节。

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

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

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

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

但它不能。因为它需要const&

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

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

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

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

const std::string &作为参数结束?

# EYZ2。许多人采纳了这个建议(包括Dave Abrahams),超越了它所适用的领域,并将其简化为适用于所有 std::string参数——总是按值传递std::string对于任何和所有任意参数和应用程序都不是“最佳实践”,因为这些演讲/文章的优化重点是应用只适用于特定的情况

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

与以往一样,通过const引用传递可以节省大量复制当你不需要拷贝的时候

现在来看看具体的例子:

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

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

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

在我看来,使用c++引用std::string是一个快速而简短的局部优化,而使用值传递可能是(或不是)一个更好的全局优化。

所以答案是:这取决于环境:

  1. 如果您编写了所有从外部到内部函数的代码,您知道代码的功能,您可以使用const std::string &引用。
  2. 如果您编写库代码或在传递字符串的地方使用大量库代码,那么通过信任std::string复制构造函数行为,您可能会在全局意义上获得更多好处。

简短回答:不!

  • # EYZ2 < br > # EYZ3
  • 如果你打算修改它或者你知道它会超出(threads)的范围,将它作为value传递,不要在函数体中复制const ref&

cpp-next.com上有一个帖子叫“要速度,要价值!”< / >强。TL;博士:

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

^的翻译

不要复制你的函数参数——意思是:如果您计划通过将参数值复制到内部变量来修改参数值,只需使用值参数即可

所以,# EYZ0:

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

# EYZ0:

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

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

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

几乎。

在c++ 17中,我们有basic_string_view<?>,这基本上把我们带到了std::string const&参数的一个狭窄用例。

move语义的存在消除了std::string const&的一个用例——如果您计划存储参数,则按值获取std::string是更优的,因为您可以从参数中删除move

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

然而,如果你不打算复制,在c++ 14中使用std::string const&仍然是有用的。

使用std::string_view,只要你没有将该字符串传递给一个期望c风格的以# eyz1结尾的字符缓冲区的API,你就可以更有效地获得类似std::string的功能,而无需承担任何分配风险。一个原始的C字符串甚至可以变成std::string_view,而不需要任何分配或字符复制。

在这一点上,std::string const&的用途是当您不批量复制数据,并将其传递给一个c风格的API,该API期望一个以空结束的缓冲区,并且您需要std::string提供的高级字符串函数。在实践中,这是一组罕见的需求。

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

为了解决这个语言问题,stl# EYZ0允许“C()”在你的例子中做一个移动语义复制无论如何,并尽职地忽略“const”关于引用计数(可变)。只要它是指定好的,这就可以了。

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

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

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*方法重载,如有没有一种方法来传递字符串文字作为参考在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

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

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

与分别为左值和右值参数定制的f的最佳实现相比,这种通用方法仅为左值和右值参数增加了move构造函数的开销。要了解为什么会这样,假设f带一个value形参,其中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赋值。因此相对而言,在这种情况下,采用const引用作为通用实现的f版本更可取。

一般来说,为了实现最优的实现,你需要重载或者做一些完美的转发就像演讲中展示的那样。缺点是所需重载数量的组合爆炸,这取决于f的参数数量,如果您选择重载参数的值类别。完美转发有一个缺点,f变成了一个模板函数,这阻止了它成为虚拟函数,如果你想100%正确地使用它,会导致代码更加复杂(详见演讲)。

看到# EYZ0。在其他主题中,他回顾了过去给出的参数传递建议,以及c++ 11中引入的新思想,并特别介绍了按值传递字符串的思想。

slide 24

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

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

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

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

如果你想知道你对这个问题的担忧有多深,请观看尼科莱Josuttis的演示并祝你好运(在发现上一个版本的错误后,“完美——完成!” n次)。你去过那里吗?)


这在标准指南中也被总结为⧺F.15


更新

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

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