我最近听了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[]。所以在我看来,通过引用传递仍然是一个好主意。
谁能解释一下赫伯为什么会这么说?
Herb Sutter和Bjarne Stroustroup一起推荐使用const std::string&作为形参类型;见https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rf-in。
这里有一个在其他答案中没有提到的陷阱:如果你将一个字符串字面值传递给一个const std::string& parameter,它将传递一个临时字符串的引用,该字符串是动态创建的,用于保存字面值的字符。如果然后保存该引用,那么一旦释放临时字符串,它将无效。为了安全起见,您必须保存副本,而不是参考资料。这个问题源于字符串字面值是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 *构造函数
常量std::string&构造函数
第二个字符串
第二个字符串
简单的回答:不!长一点的回答:
如果不修改字符串(treat是只读的),则将其作为const ref&传递。(const ref&显然需要在使用它的函数执行时保持在作用域内)
如果你打算修改它,或者你知道它将超出范围(线程),将它作为一个值传递,不要在函数体中复制const ref&。
在cpp-next.com网站上有一篇文章叫做“想要速度,而不是价值!”TL;博士:
指南:不要复制函数参数。相反,应该按值传递它们,并让编译器执行复制。
^的翻译
不要复制你的函数实参——意思是:如果你打算通过将实参复制到内部变量来修改实参值,只需使用一个值实参即可。
所以,不要这样做:
std::string function(const std::string& aString){
auto vString(aString);
vString.clear();
return vString;
}
这样做:
std::string function(std::string aString){
aString.clear();
return aString;
}
当您需要修改函数体中的参数值时。
您只需要注意计划如何在函数体中使用参数。只读或非只读…如果它在范围内。
我复制/粘贴了这个问题的答案,并更改了名称和拼写以适应这个问题。
下面是用来衡量问题的代码:
#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构造。对于任何特定的情况,这可能是可接受的,也可能是不可接受的。这两种解决方案各有优缺点。
这在很大程度上取决于编译器的实现。
然而,这也取决于你使用什么。
让我们考虑下一个函数:
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在这方面并没有真正改变任何东西,除了那些不相信复制省略的人。