c++ string传参




传递const std:: string&作为参数的日子已经过去了吗? (10)

传递const std :: string&作为参数的日子已经过去了吗?

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

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

与往常一样,在不需要副本的情况下 ,通过const引用传递会节省大量复制。

现在来看具体的例子:

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

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

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

我听到Herb Sutter最近的一次谈话,他建议通过const & std::vectorstd::string在很大程度上没有了。 他建议编写一个如下的函数现在更可取:

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

我明白return_val在函数返回时是一个右值,因此可以使用移动语义返回,这很便宜。 但是, 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&参数,它将传递一个临时字符串的引用,该字符串正在动态创建以保存字符文字。 如果您保存该引用,那么一旦临时字符串被释放,它将无效。 为了安全起见,您必须保存副本 ,而不是参考。 问题源于字符串文字是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
}

OUTPUT:

const char * constructor
const std::string& constructor

Second string
Second string

NO

为什么不? 如果你有10 ^ 256整数的矢量,研究机构不需要改变呢? 如果您每次需要使用该矢量时都会发生什么情况? 我同意@ NicolBolas的回答,但我不同意Herb Sutter先生。

以CERN项目为例,他们在一秒钟内收集了大量数据,对我来说,让每个人都弄糟原始数据并不是一个好主意。


std::string使用C ++参考的IMO是一种快速和简短的本地优化,而使用按值传递可能是(或不)更好的全局优化。

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

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

参见“Herb Sutter”“回到基础!现代C ++风格的基础” 。除了其他主题,他还回顾了过去给出的参数传递建议,以及C ++ 11中提出的新思路,并专门研究了按价值传递字符串的想法。

基准测试表明,在函数将其复制的情况下,通过值传递std::string s会明显变慢!

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

请参阅他的幻灯片27:对于“设置”功能,选项1与以往一样。 选项2为右值引用添加了一个重载,但是如果有多个参数,则会发生组合爆炸。

它仅适用于必须创建字符串(不改变其现有值)的“汇”参数,以确保传值技巧的有效性。 也就是说,参数直接初始化匹配类型的成员的构造函数

如果你想看看你有多深可以去担心这个问题,那么看看Nicolai Josuttis的演讲并祝你好运(在完成之前的版本中, “完美 - 完成!” n次)。


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

这里是测量问题的代码:

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

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


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

  • 如果您不会修改字符串(对待为只读),请将其作为const ref&传递。
    const ref&显然需要保持在范围内,而使用它的函数执行)
  • 如果你打算修改它,或者你知道它会超出作用域(线程) ,把它作为一个value传递,不要复制const ref&在函数体内部。

cpp-next.com上有一篇文章称为“想要速度,按价值传递!” 。 TL; DR:

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

^的翻译

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

所以, 不要这样做

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

做到这一点

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

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

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


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

比方说,我有函数A调用函数B调用函数C A将一个字符串通过B传递给C A不知道或不关心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的移动语义应该能够去除不必要的拷贝构造,对吗? A通过临时; 没有理由为什么C应该复制数据。 它应该只是泄露给它的东西。

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

如果我改变C以按值取其参数,那只会导致B对该参数进行复制; 我什么也得不到。

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

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

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


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

为了解决这个语言问题,STL 可以允许你的示例中的“C()” 无论如何都要创建一个移动语义副本,并且尽量忽略关于引用计数的“const”(因此假设它不是声明const,因为它是mem-mapped或nano-thready或其他)。 只要它是明确的,这将是很好的。

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

由于C ++在这里不提供const粒度,所以编写一个好的规范并制作一个闪亮的新“const可移动字符串”(cmstring)对象是我见过的最好的解决方案。


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

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

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

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

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





c++11