是传递const std :: string&作为参数的日子吗?
我听到了Herb Sutter最近的一次谈话,他build议通过const &
std::vector
和std::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到C
。 A
不知道或不关心C
; 所有A
知道的是B
也就是说, C
是B
的实现细节。
假设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::string
function,而不会冒任何分配的风险。 一个原始的Cstring甚至可以变成std::string_view
而不需要任何分配或字符复制。
在这一点上, std::string const&
是在你不复制批量数据的时候,并且要把它传递给一个C样式的API,它需要一个空终止的缓冲区,并且你需要更高级的stringstd::string
提供的函数。 在实践中,这是一个罕见的要求。
国际海事组织使用std::string
的C ++参考是一个快速和简短的本地优化,而传递值可以(或不)更好的全局优化。
所以答案是:这取决于情况:
- 如果你把所有的代码从外部写到内部函数中,你知道代码是干什么的,你可以使用引用
const std::string &
。 - 如果您编写库代码或在传递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引用在函数调用后仍然有效(所以不要复制引用!)。