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

我听到了Herb Sutter最近的一次谈话,他build议通过const & std::vectorstd::string的原因很大程度上消失了。 他build议现在写一个如下的函数是比较好的:

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

我知道return_val在函数返回的位置是一个右值,因此可以使用移动语义来返回,这很便宜。 然而, inval仍然大于参考的大小(通常作为指针实现)。 这是因为std::string具有各种组件,包括一个指向堆的指针和一个用于短string优化的成员char[] 。 所以在我看来,通过引用仍然是一个好主意。

任何人都可以解释为什么赫布可能会这样说?

赫伯说他说的是因为这样的情况。

比方说,我有函数A调用函数B调用函数C A通过B传递一个string到CA不知道或不关心C ; 所有A知道的是B 也就是说, CB的实现细节。

假设A被定义如下:

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

如果B和C通过const&获取string,那么它看起来像这样:

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

一切顺利。 你只是通过指针,不复制,不移动,每个人都很高兴。 C接受一个const&因为它不存储string。 它只是使用它。

现在,我想做一个简单的改变: C需要存储string的地方。

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

你好,复制构造函数和潜在的内存分配(忽略短string优化(SSO) )。 C ++ 11的移动语义应该能够去除不必要的拷贝构造,对吗? A通过临时; C没有理由要复制数据。 它应该只是泄漏给它的东西。

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

如果我改变C参数的值,这只是导致B做复制到该参数; 我一无所获

所以如果我只是通过所有的函数来传递值,那么依靠std::move来转换数据,我们不会有这个问题。 如果有人想坚持下去,他们可以。 如果他们不这样做,那好吧。

它更昂贵吗? 是; 移入一个值比使用引用更昂贵。 比副本便宜吗? 不适用于带有SSO的小string。 值得这样做吗?

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

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

没有 。 许多人把这个build议(包括Dave Abrahams)超出了它所适用的领域,并简化它适用于所有的 std::string参数 – 总是通过值传递std::string不是一个“最佳实践”的任何和所有的任意参数和应用程序,因为这些会谈/文章所关注的优化适用于有限的一组案例

如果你返回一个值,改变参数或者取值,那么通过值传递可以节省昂贵的复制,并提供语法上的便利。

和往常一样, 当你不需要拷贝 ,通过const引用传递会节省很多拷贝

现在来看具体的例子:

然而,inval仍然比引用(通常作为指针实现)的大小大很多。 这是因为std :: string具有各种组件,包括一个指向堆的指针和一个用于短string优化的成员char []。 所以在我看来,通过引用仍然是一个好主意。 任何人都可以解释为什么赫布可能会这样说?

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

通过const引用传递的日子还没有结束 – 规则比以前更复杂了。 如果性能很重要,那么根据您在实现中使用的细节,考虑如何传递这些types是明智的。

这高度依赖于编译器的实现。

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

让我们考虑下一个function:

 bool foo1( const std::string v ) { return v.empty(); } bool foo2( const std::string & v ) { return v.empty(); } 

这些函数在一个单独的编译单元中执行,以避免内联。 然后 :
如果你把这两个函数传给一个文字,你将不会在性能上看到很大的差别。 在这两种情况下,都必须创build一个string对象
2.如果你传递另一个std :: string对象, foo2将会胜过foo1 ,因为foo1会做一个深层复制。

在我的电脑上,使用g ++ 4.6.1,我得到了这些结果:

  • 通过引用variables:1000000000次迭代 – >经过时间:2.25912秒
  • 按值变化:1000000000次迭代 – >经过时间:27.2259秒
  • 通过参考文字:100000000次迭代 – >经过的时间:9.10319秒
  • 按值计算:100000000次迭代 – >经过时间:8.62659秒

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

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

如果你改变这个值来接受string,那么你将最终移动或复制参数,并没有这个需要。 不仅复制/移动可能更昂贵,而且还会引入新的潜在失败; 复制/移动可能会引发exception(例如,复制期间的分配可能失败),而引用现有值则不能。

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


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

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

  • 如果你不会修改string(treat是只读的),把它作为const ref&传递。
    const ref&显然需要留在范围内,而使用它的函数执行)
  • 如果你打算修改它,或者你知道它会超出作用域(线程) ,把它作为一个value传递,不要复制const ref&内部的函数体。

cpp-next.com上有一个名为“想要速度,按价值传递! 。 TL; DR:

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

^的翻译

不要复制你的函数参数 —意思是: 如果你打算通过将参数值复制到一个内部variables来修改参数值,只需要使用一个值参数

所以, 不要这样做

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

这样做

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

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

你只需要知道你打算如何使用函数体中的参数。 只读或不…,如果它坚持在范围内。

std::string不是普通旧数据(POD) ,它的原始大小不是最相关的东西。 例如,如果你传递一个超过SSO长度并在堆上分配的string,我期望复制构造函数不复制SSO存储。

build议这样做的原因是因为inval由参数expression式构造,因此总是被移动或复制(如果适当的话),假设您需要拥有参数,则不会有性能损失。 如果你不这样做, const引用仍然是更好的方法。

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

这里是测量被问到的内容的代码:

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

传值解决scheme只需要一个超载,但是在传递左值和右值时需要额外的移动构造。 对于任何特定情况,这可能会也可能不会被接受。 两种解决scheme都有优点和缺点。

Herb Sutter和Bjarne Stroustroup一起在推荐const std::string&作为参数types时仍然logging在案; 请参阅https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in

在这里的任何其他答案中都没有提到一个陷阱:如果你将一个string文字传递给一个const std::string&参数,它将传递一个临时string的引用,这个string被dynamic创build以保存字符文字。 如果您保存该引用,则一旦临时string被释放,该引用将无效。 为了安全起见,您必须保存一份副本 ,而不是参考文献。 问题源于string文字是const char[N]types的事实,需要升级到std::string

下面的代码说明了陷阱和解决方法,以及一个小的效率选项 – 用const char*方法重载, 有没有一种方法可以在C ++中传递string作为参考 。

(注意:Sutter&Stroustroupbuild议如果你保留一个string的副本,还要用&&参数和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 } 

OUTPUT:

 const char * constructor const std::string& constructor Second string Second string 

几乎。

basic_string_view<?>有一个TS,如果被批准并且被折叠成C ++ 17,将会把我们带到基本上std::string const& parameters的一个狭窄用例。

移动语义的存在已经消除了std::string const&一个用例std::string const& – 如果你打算存储参数,按值取std::string更为合适,因为你可以move参数。

如果有人用原始的C "string"调用你的函数,这意味着只有一个std::string缓冲区被分配,而std::string const& case中的两个。

但是,如果你不打算复制,那么通过std::string const&在C ++ 14中仍然是有用的。

使用std::string_view ,只要你没有将string传递给需要C风格'\0'终止的字符缓冲区的API,就可以更有效地获得std::stringfunction,而不会冒任何分配的风险。 一个原始的Cstring甚至可以变成std::string_view而不需要任何分配或字符复制。

在这一点上, std::string const&是在你不复制批量数据的时候,并且要把它传递给一个C样式的API,它需要一个空终止的缓冲区,并且你需要更高级的stringstd::string提供的函数。 在实践中,这是一个罕见的要求。

国际海事组织使用std::string的C ++参考是一个快速和简短的本地优化,而传递值可以(或不)更好的全局优化。

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

  1. 如果你把所有的代码从外部写到内部函数中,你知道代码是干什么的,你可以使用引用const std::string &
  2. 如果您编写库代码或在传递string的地方使用大量库代码,则通过信任std::string复制构造函数行为,您可能获得更多的全局意义。

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

为了解决这个语言问题,STL 可以在你的例子中允许“C()”做一个移动语义拷贝,并尽可能地忽略关于引用计数的“const”(因此假设它不是声明const,因为它是mem-mapped或nano-thready或其他)。 只要它是明确的,这将是没有问题的。

由于STL没有,我有一个const_casts <>引用计数器的string版本,并且 – 你可以自由地传递cmstring作为常量引用,并将它们复制到深层函数中,整天,没有泄漏或问题。

由于C ++在这里没有提供const粒度,所以编写一个好的规范并制作一个新的“const可移动string”(cmstring)对象是我见过的最好的解决scheme。

没有银弹。 像往常一样,这取决于你的用例。

在我的情况下,我倾向于使用值参数,我有一个function,所谓的汇参数。 sink参数的值被复制到函数体中。 在这种情况下,您按值传递,以便您可以移动构造或从传递的参数中移动赋值。 请参阅: 我应该总是移动`sink`构造函数或setter参数吗? 。

在其他情况下,您总是可以想出一个场景,其中const refeference参数比使用value参数更有效率,特别是当函数的参数是具有昂贵的复制语义的左值时。 将一个右值传递给一个const引用永远不会坏,它只是延长了临时的生命周期,缺点是你不能安全的假定const引用在函数调用后仍然有效(所以不要复制引用!)。