Are the days of passing const std::string & as a parameter over?
我听到赫伯·萨特最近的一次谈话,他说,通过
1 2 3 4 5 6 | std::string do_something ( std::string inval ) { std::string return_val; // ... do stuff ... return return_val; } |
我理解
有人能解释为什么赫伯会这么说吗?
赫伯之所以这么说是因为这种情况。
假设我有函数
假设a的定义如下:
1 2 3 4 | void A() { B("value"); } |
如果b和c用
1 2 3 4 5 6 7 8 9 | void B(const std::string &str) { C(str); } void C(const std::string &str) { //Do something with `str`. Does not store it. } |
一切都很好。你只是在传递指针,不复制,不移动,每个人都很高兴。
现在,我想做一个简单的更改:
1 2 3 4 5 | void C(const std::string &str) { //Do something with `str`. m_str = str; } |
您好,复制构造函数和潜在的内存分配(忽略短字符串优化(SSO))。C++ 11的移动语义应该能够消除不必要的拷贝构造,对吗?
但它不能,因为它需要一个
如果我把
所以,如果我刚刚通过所有函数按值传递
它更贵吗?是的;转换为值比使用引用更昂贵。它比这本书便宜吗?不适用于带有SSO的小字符串。值得做吗?
这取决于您的用例。你有多讨厌内存分配?
Are the days of passing const std::string & as a parameter over?
不。许多人接受这个建议(包括Dave Abrahams)超出了它所适用的领域,并简化它以应用于所有
如果返回值、改变参数或获取值,那么通过值传递可以节省昂贵的复制成本,并提供语法上的便利。
和以往一样,传递常量引用在不需要副本的情况下可以节省很多副本。
下面是具体的例子:
However inval is still quite a lot larger than the size of a reference (which is usually implemented as a pointer). This is because a std::string has various components including a pointer into the heap and a member char[] for short string optimization. So it seems to me that passing by reference is still a good idea. Can anyone explain why Herb might have said this?
如果需要考虑堆栈大小(假设这不是内联/优化的),那么
通过引用常量的日子并没有结束——规则比以前更复杂了。如果性能很重要,那么根据在实现中使用的细节,考虑如何传递这些类型是明智的。
这高度依赖于编译器的实现。
但是,它还取决于您使用的是什么。
让我们考虑下一个函数:
1 2 3 4 5 6 7 8 | bool foo1( const std::string v ) { return v.empty(); } bool foo2( const std::string & v ) { return v.empty(); } |
这些函数在单独的编译单元中实现,以避免内联。然后:1。如果您将一个文本传递给这两个函数,您将不会看到性能上的很大差异。在这两种情况下,都必须创建一个字符串对象2。如果传递另一个std::string对象,
在我的电脑上,使用G++4.6.1,我得到了以下结果:
- 参考变量:100000000次迭代->已用时间:2.25912秒
- 按值可变:100000000次迭代->已用时间:27.2259秒
- 引用文本:100000000次迭代->已用时间:9.10319秒
- 按值逐字:100000000次迭代->已用时间:8.62659秒
简短回答:不!长回答:
- 如果不修改字符串(视为只读),则将其作为
const ref& 传递。(const ref& 显然需要在作用域内,同时使用它的函数执行) - 如果您计划修改它,或者您知道它将超出范围(线程),那么将它作为一个
value 传递,不要在您的函数体中复制const ref& 。
在cpp-next.com上有一个帖子叫做"想要速度,通过值!"TL;DR:
Guideline: Don’t copy your function arguments. Instead, pass them by value and let the compiler do the copying.
翻译^
不要复制函数参数---意思是:如果您计划通过将参数值复制到一个内部变量来修改它,只需要使用一个值参数。
所以,不要这样做:
1 2 3 4 5 | std::string function(const std::string& aString){ auto vString(aString); vString.clear(); return vString; } |
这样做:
1 2 3 4 | std::string function(std::string aString){ aString.clear(); return aString; } |
当需要修改函数体中的参数值时。
您只需要知道如何计划在函数体中使用参数。是否只读…如果它在范围内。
除非你真的需要一份副本,否则拿走
1 2 3 | bool isprint(std::string const &s) { return all_of(begin(s),end(s),(bool(*)(char))isprint); } |
如果您将其更改为按值获取字符串,那么您将最终移动或复制参数,而不需要这样做。复制/移动不仅可能更昂贵,而且还会带来新的潜在故障;复制/移动可能引发异常(例如,复制期间的分配可能失败),而引用现有值则不可能。
如果您确实需要一个副本,那么按值传递和返回通常是(总是?)最好的选择。事实上,除非你发现额外的副本会导致性能问题,否则我一般不会在C++ 03中担心它。在现代编译器中,副本省略似乎相当可靠。我认为人们对于必须检查对rvo的编译器支持表的怀疑和坚持现在已经过时了。
简而言之,C++ 11并没有真正改变这方面的内容,除了那些不相信拷贝删除的人。
几乎。
在C++ 17中,我们有EDOCX1,4,这使我们基本上是一个EDCOX1×5参数的一个狭窄的用例。
移动语义的存在消除了
如果有人用一个原始的c
但是,如果您不打算复制,EDCOX1的引用(5)在C++ 14中仍然是有用的。
使用
此时,
我已将此问题的答案复制/粘贴到此处,并更改了名称和拼写以适合此问题。
下面是用来测量被询问内容的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | #include <iostream> struct string { string() {} string(const string&) {std::cout <<"string(const string&) ";} string& operator=(const string&) {std::cout <<"string& operator=(const string&) ";return *this;} #if (__has_feature(cxx_rvalue_references)) string(string&&) {std::cout <<"string(string&&) ";} string& operator=(string&&) {std::cout <<"string& operator=(string&&) ";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: "; string x; string t = do_something(x); #if (__has_feature(cxx_rvalue_references)) std::cout <<" do_something with xvalue: "; string u = do_something(std::move(x)); #endif std::cout <<" do_something with prvalue: "; string v = do_something(source()); } |
对于我来说,这个输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | $ 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&&) |
下表总结了我的结果(使用CLAN-STD= C++ 11)。第一个数字是复制构造的数目,第二个数字是移动构造的数目:
1 2 3 4 5 6 7 | +----+--------+--------+---------+ | | lvalue | xvalue | prvalue | +----+--------+--------+---------+ | p1 | 1/1 | 0/2 | 0/1 | +----+--------+--------+---------+ | p2 | 1/0 | 0/1 | 0/1 | +----+--------+--------+---------+ |
传递值解决方案只需要一个重载,但在传递lvalues和xvalues时需要额外的移动构造。对于任何给定的情况,这可能是可接受的,也可能是不可接受的。这两种解决方案都有优点和缺点。
之所以建议这样做,是因为
Herb Sutter和Bjarne StroustGroup在推荐
这里还有一个陷阱,在其他任何答案中都没有提到:如果将字符串文字传递给
下面的代码说明了陷阱和解决方法,以及一个小的效率选项——用EDCOX1的4Ω方法重载,如在C++中有一种方法来传递字符串文本作为参考。
(注意:sutter&stroustgroup建议,如果保留字符串的副本,还应提供一个带有&;参数和std::move()的重载函数。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #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 ";} 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 ";} 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 <<" "; // garbage out std::cout << w2.myStrCopy <<" "; // OK std::cout << w3.myStrCopy <<" "; // OK } |
输出:
1
2
3
4
5 const char * constructor
const std::string& constructor
Second string
Second string
IMO使用C++引用EDCOX1×23是一个快速和简短的局部优化,而使用传递值可以是(或不是)更好的全局优化。
所以答案是:这取决于环境:
见"药草萨特"回到基本!现代C++风格精要在其他主题中,他回顾了过去给出的参数传递建议,以及用C++ 11实现的新思想,并具体地讨论了通过值传递字符串的思想。
基准测试表明,在函数无论如何都要复制它的情况下,按值传递
这是因为您强制它始终进行完整复制(然后移动到位),而
看他的幻灯片27:关于"设置"功能,选项1和以前一样。选项2为右值引用添加了一个重载,但如果有多个参数,则会出现组合爆炸。
只有在必须创建字符串(不更改其现有值)的"接收器"参数中,传递值技巧才有效。也就是说,参数直接初始化匹配类型的成员的构造函数。
如果你想知道你能在多大程度上担心这一点,看看尼科莱·乔舒蒂的演讲,祝你好运("完美完成!"发现前一版本的错误后N次。去过吗?)
这也概括为?标准指南中的F.15。
作为@ JD?乌戈斯在评论中指出,赫伯在另一篇文章中给出了其他建议(稍后?)谈谈,大致见:https://youtu.be/xnqtkd8ud64?T=54 M50S。
他的建议归根结底就是只对接受所谓的sink参数的函数
与分别针对lvalue和rvalue参数定制的
1 2 3 | void f(T x) { T y{std::move(x)}; } |
用左值参数调用
一般来说,左值参数的
1 2 3 | void f(const T& x) { T y{x}; } |
在这种情况下,只调用一个复制构造函数来构造
1 2 3 | void f(T&& x) { T y{std::move(x)}; } |
在这种情况下,只调用一个move构造函数来构造
因此,一个合理的折衷方案是采用一个值参数,并对优化实现的左值或右值参数进行一个额外的移动构造函数调用,这也是Herb讨论中给出的建议。
作为@ JD?Ugosz在注释中指出,传递值只对将从sink参数构造某些对象的函数有意义。当您有一个复制其参数的函数
1 2 3 4 5 | void f(T x) { T y{...}; ... y = std::move(x); } |
在这种情况下,左值参数有复制构造和移动赋值,右值参数有移动构造和移动赋值。左值参数的最理想情况是:
1 2 3 4 5 | void f(const T& x) { T y{...}; ... y = x; } |
这可以归结为一个赋值,它可能比复制构造函数加上传递值方法所需的移动赋值便宜得多。这样做的原因是,分配可能会重用
对于右值参数,保留副本的
1 2 3 4 5 | void f(T&& x) { T y{...}; ... y = std::move(x); } |
所以,在这种情况下,只有移动分配。将一个右值传递给接受常量引用的
因此,一般来说,对于最理想的实现,您需要过载或执行某种完美的转发,如本文所示。缺点是,如果您选择在参数的值类别上重载,那么根据EDOCX1的参数数量(0),所需的重载数量会出现组合爆炸。完美转发有一个缺点,即
问题是"const"是一个非粒度限定符。"const string ref"通常的意思是"不要修改这个字符串",而不是"不要修改引用计数"。在C++中,根本没有办法说出哪些成员是"const"。他们要么都是,要么都不是。
为了解决这个语言问题,STL可以允许您的示例中的"c()"无论如何进行移动语义复制,并尽职地忽略与引用计数(可变)相关的"const"。只要有明确的规定,就可以了。
因为STL没有,所以我有一个字符串版本,它const casts<>a way引用计数器(在类层次结构中没有可追溯的方法使某些内容变为可变的),并且-lo和look-您可以自由地将cmstring作为const引用传递,并在深层函数中复制它们,一整天都没有泄漏或问题。
因为C++在这里不提供"派生类const粒度",所以编写一个好的规范并创建一个闪亮的新的"const可移动字符串"(CMstring)对象是我所见过的最好的解决方案。