关于c ++:传递const std :: string&的日子。

Are the days of passing const std::string & as a parameter over?

我听到赫伯·萨特最近的一次谈话,他说,通过std::vectorstd::string的理由基本上已经不复存在了。他建议现在最好编写如下函数:

1
2
3
4
5
6
std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}

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

有人能解释为什么赫伯会这么说吗?


赫伯之所以这么说是因为这种情况。

假设我有函数A,它调用函数B,它调用函数C。而A则通过B进入C中。A不知道也不关心CA知道的都是B。也就是说,CB的一个实现细节。

假设a的定义如下:

1
2
3
4
void A()
{
  B("value");
}

如果b和c用const&取字符串,则看起来如下:

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.
}

一切都很好。你只是在传递指针,不复制,不移动,每个人都很高兴。C接受const&,因为它不存储字符串。它只是简单地使用它。

现在,我想做一个简单的更改:C需要将字符串存储在某个地方。

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

您好,复制构造函数和潜在的内存分配(忽略短字符串优化(SSO))。C++ 11的移动语义应该能够消除不必要的拷贝构造,对吗?A通过了一个临时的;没有理由要求C必须复制数据。它应该和给它的东西一起潜逃。

但它不能,因为它需要一个const&

如果我把C改为按值取其参数,这只会导致B复制到该参数中,我什么也得不到。

所以,如果我刚刚通过所有函数按值传递str,依靠std::move来改变数据,我们就不会有这个问题。如果有人想坚持下去,他们可以。如果他们没有,哦,好吧。

它更贵吗?是的;转换为值比使用引用更昂贵。它比这本书便宜吗?不适用于带有SSO的小字符串。值得做吗?

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


Are the days of passing const std::string & as a parameter over?

不。许多人接受这个建议(包括Dave Abrahams)超出了它所适用的领域,并简化它以应用于所有std::string参数——总是按值传递std::string不是任何和所有任意参数和应用程序的"最佳实践",因为这些讨论/文章的优化只适用于限制条件。选择了一组案例。

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

和以往一样,传递常量引用在不需要副本的情况下可以节省很多副本。

下面是具体的例子:

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?

如果需要考虑堆栈大小(假设这不是内联/优化的),那么return_valinvalreturn_val+iow,可以通过在此处传递值来减少峰值堆栈使用(注:abis过于简单化)。同时,传递常量引用可以禁用优化。这里的主要原因不是为了避免堆栈增长,而是为了确保可以在适用的地方执行优化。

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


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

但是,它还取决于您使用的是什么。

让我们考虑下一个函数:

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对象,foo2将优于foo1,因为foo1将进行深度复制。

在我的电脑上,使用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;
}

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

您只需要知道如何计划在函数体中使用参数。是否只读…如果它在范围内。


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

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参数的一个狭窄的用例。

移动语义的存在消除了std::string const&的一个用例——如果您计划存储参数,那么按值取std::string更为理想,因为您可以从参数中提取move

如果有人用一个原始的c "string"调用函数,这意味着只分配了一个std::string缓冲区,而在std::string const&情况下则分配了两个。

但是,如果您不打算复制,EDCOX1的引用(5)在C++ 14中仍然是有用的。

使用std::string_view,只要不将所述字符串传递给期望使用C样式'\0'终止字符缓冲区的API,就可以更有效地获得std::string类功能,而不必冒任何分配风险。原始C字符串甚至可以在不进行任何分配或字符复制的情况下转换为std::string_view

此时,std::string const&的用途是在不批量复制数据的情况下,将数据传递给C样式的API,该API需要一个以空结尾的缓冲区,并且需要std::string提供的更高级别的字符串函数。在实践中,这是一组罕见的需求。


我已将此问题的答案复制/粘贴到此处,并更改了名称和拼写以适合此问题。

下面是用来测量被询问内容的代码:

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时需要额外的移动构造。对于任何给定的情况,这可能是可接受的,也可能是不可接受的。这两种解决方案都有优点和缺点。


std::string不是普通的旧数据(pod),它的原始大小也不是最相关的。例如,如果传入的字符串超过了SSO的长度并在堆上分配,我希望复制构造函数不会复制SSO存储。

之所以建议这样做,是因为inval是根据参数表达式构造的,因此总是根据需要移动或复制—假设您需要参数的所有权,则不会造成性能损失。如果你不这样做,一个const参考仍然是更好的方法。


Herb Sutter和Bjarne StroustGroup在推荐const std::string&作为参数类型方面仍有记录;请参阅https://github.com/isocpp/cppcoreguidelines/blob/master/cppcoreguidelines.md rf in。

这里还有一个陷阱,在其他任何答案中都没有提到:如果将字符串文字传递给const std::string&参数,它将传递对临时字符串的引用,该临时字符串是动态创建的,用于保存该文字的字符。如果然后保存该引用,则一旦解除分配临时字符串,该引用将无效。为了安全起见,您必须保存一个副本,而不是引用。这个问题源于字符串是const char[N]类型,需要升级到std::string类型。

下面的代码说明了陷阱和解决方法,以及一个小的效率选项——用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&amp; constructor

Second string
Second string


IMO使用C++引用EDCOX1×23是一个快速和简短的局部优化,而使用传递值可以是(或不是)更好的全局优化。

所以答案是:这取决于环境:

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

  • 见"药草萨特"回到基本!现代C++风格精要在其他主题中,他回顾了过去给出的参数传递建议,以及用C++ 11实现的新思想,并具体地讨论了通过值传递字符串的思想。

    slide 24

    基准测试表明,在函数无论如何都要复制它的情况下,按值传递std::string的速度可能会明显减慢!

    这是因为您强制它始终进行完整复制(然后移动到位),而const&版本将更新旧字符串,旧字符串可能会重用已分配的缓冲区。

    看他的幻灯片27:关于"设置"功能,选项1和以前一样。选项2为右值引用添加了一个重载,但如果有多个参数,则会出现组合爆炸。

    只有在必须创建字符串(不更改其现有值)的"接收器"参数中,传递值技巧才有效。也就是说,参数直接初始化匹配类型的成员的构造函数。

    如果你想知道你能在多大程度上担心这一点,看看尼科莱·乔舒蒂的演讲,祝你好运("完美完成!"发现前一版本的错误后N次。去过吗?)

    这也概括为?标准指南中的F.15。


    作为@ JD?乌戈斯在评论中指出,赫伯在另一篇文章中给出了其他建议(稍后?)谈谈,大致见:https://youtu.be/xnqtkd8ud64?T=54 M50S。

    他的建议归根结底就是只对接受所谓的sink参数的函数f使用值参数,假设您将从这些sink参数中移动构造。

    与分别针对lvalue和rvalue参数定制的f的优化实现相比,这种通用方法只增加了lvalue和rvalue参数的移动构造函数的开销。为了了解这种情况的原因,假设f采用一个值参数,其中T是某种可复制和移动的可构造类型:

    1
    2
    3
    void f(T x) {
      T y{std::move(x)};
    }

    用左值参数调用f将导致调用复制构造函数来构造x,调用move构造函数来构造y。另一方面,用右值参数调用f将导致调用move构造函数来构造x,调用另一个move构造函数来构造y

    一般来说,左值参数的f的最佳实现如下:

    1
    2
    3
    void f(const T& x) {
      T y{x};
    }

    在这种情况下,只调用一个复制构造函数来构造y。对于右值参数,f的最佳实现通常如下:

    1
    2
    3
    void f(T&& x) {
      T y{std::move(x)};
    }

    在这种情况下,只调用一个move构造函数来构造y

    因此,一个合理的折衷方案是采用一个值参数,并对优化实现的左值或右值参数进行一个额外的移动构造函数调用,这也是Herb讨论中给出的建议。

    作为@ JD?Ugosz在注释中指出,传递值只对将从sink参数构造某些对象的函数有意义。当您有一个复制其参数的函数f时,传递值方法将比一般的传递常量引用方法有更多的开销。保留参数副本的函数f的传递值方法的形式如下:

    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;
    }

    这可以归结为一个赋值,它可能比复制构造函数加上传递值方法所需的移动赋值便宜得多。这样做的原因是,分配可能会重用y中现有的已分配内存,因此会阻止(取消)分配,而复制构造函数通常会分配内存。

    对于右值参数,保留副本的f的最佳实现形式为:

    1
    2
    3
    4
    5
    void f(T&& x) {
      T y{...};
      ...
      y = std::move(x);
    }

    所以,在这种情况下,只有移动分配。将一个右值传递给接受常量引用的f版本只需要一个赋值,而不需要移动赋值。因此,相对而言,在这种情况下,将常量引用作为一般实现的f版本更可取。

    因此,一般来说,对于最理想的实现,您需要过载或执行某种完美的转发,如本文所示。缺点是,如果您选择在参数的值类别上重载,那么根据EDOCX1的参数数量(0),所需的重载数量会出现组合爆炸。完美转发有一个缺点,即f变成了一个模板函数,这会阻止它成为虚拟的,并且如果你想100%正确地得到它,会导致非常复杂的代码(有关详细信息,请参见谈话)。


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

    为了解决这个语言问题,STL可以允许您的示例中的"c()"无论如何进行移动语义复制,并尽职地忽略与引用计数(可变)相关的"const"。只要有明确的规定,就可以了。

    因为STL没有,所以我有一个字符串版本,它const casts<>a way引用计数器(在类层次结构中没有可追溯的方法使某些内容变为可变的),并且-lo和look-您可以自由地将cmstring作为const引用传递,并在深层函数中复制它们,一整天都没有泄漏或问题。

    因为C++在这里不提供"派生类const粒度",所以编写一个好的规范并创建一个闪亮的新的"const可移动字符串"(CMstring)对象是我所见过的最好的解决方案。