将const std :: string&作为参数传递的日子是多少?

Ben*_*enj 579 c++ c++11

我听取了香草萨特最近的谈话谁建议的理由来传递std::vectorstd::string利用const &在很大程度上消失了.他建议现在更好地编写如下函数:

std::string do_something ( std::string inval )
{
   std::string return_val;
   // ... do stuff ...
   return return_val;
}
Run Code Online (Sandbox Code Playgroud)

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

谁能解释为什么Herb可能会说这个?

Nic*_*las 381

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

假设我有A调用函数的函数B,它调用函数C.并A传递一个字符串B进入C.A不知道或不关心C; 所有人都A知道B.也就是说,C是一个实现细节B.

假设A定义如下:

void A()
{
  B("value");
}
Run Code Online (Sandbox Code Playgroud)

如果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.
}
Run Code Online (Sandbox Code Playgroud)

一切都很好.你只是传递指针,没有复制,没有移动,每个人都很开心.C采用a const&因为它不存储字符串.它只是使用它.

现在,我想做一个简单的改变:C需要将字符串存储在某处.

void C(const std::string &str)
{
  //Do something with `str`.
  m_str = str;
}
Run Code Online (Sandbox Code Playgroud)

您好,复制构造函数和潜在的内存分配(忽略短字符串优化(SSO)).C++ 11的移动语义应该可以删除不必要的复制构造,对吧?并A通过临时; 没有理由C复制数据.它应该只是潜逃而已.

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

如果我更改C为按值获取其参数,那只会导致B复制到该参数; 我一无所获.

因此,如果我刚刚str通过所有函数传递值,依赖于std::move对数据进行混洗,我们就不会遇到这个问题.如果有人想坚持下去,他们可以.如果他们不这样做,哦.

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

这取决于您的使用案例.你讨厌内存分配多少钱?

  • @ildjarn:在顺序分析中,如果某事的最坏情况受常数约束,那么它仍然是恒定时间.没有最长的小字符串吗?这个字符串不需要花费一些时间来复制吗?不是所有小字符串都需要更少的时间来复制吗?然后,对于小字符串的字符串复制在顺序分析中是"恒定时间" - 尽管小字符串需要不同的时间来复制.订单分析涉及*渐近行为*. (16认同)
  • 为什么字符串在by值的情况下从B移动到C?如果B是`B(std :: string b)`而C是`C(std :: string c)`那么我们必须在B或`b中调用`C(std :: move(b))`必须保持不变(因此'从'移动')直到退出'B`.(如果在调用后没有使用`b`但是我认为没有强有力的保证,那么优化编译器可能会在as-if规则下移动字符串.)对于`str`的​​副本也是如此.到`m_str`.即使函数参数用rvalue初始化,它也是函数内的左值,并且`std :: move`需要从该左值移动. (11认同)
  • @NeilG:当然,但你的**原始**问题是"*仍然更加昂贵的数量(与被移动的字符串的长度无关)对吗?*"我想说的是,它_different_常量可能会更昂贵,具体取决于字符串的长度,总结为"否". (6认同)
  • “因此,如果我只是通过所有函数按值传递 str,并依靠 std::move 来洗牌数据,...”也许您可以显示您想到的更好的版本,包括 std::动作会被放置?这并不明显。 (4认同)
  • @NeilG:你明白"依赖于实现"是什么意思吗?你所说的是错的,因为它取决于_if以及如何实施SSO. (3认同)
  • 当你说移动到一个值比使用引用更昂贵时,这仍然是一个恒定的数量(与所移动的字符串的长度无关)更昂贵吗? (2认同)
  • 抱歉挑剔,但您没有在示例中的任何地方指定 m_str 的类型。如果它是一个 `const std::string&`,就没有复制构造函数,没有开销。如果您将其类型明确化,那么对于 C++ 的新手来说,您的答案将更容易理解。 (2认同)

jus*_*tin 154

将const std :: string&作为参数传递的日子是多少?

.许多人将此建议(包括Dave Abrahams)超出其适用的域,并简化它以应用于所有 std::string参数 - 始终通过std::string值传递不是任何和所有任意参数和应用程序的"最佳实践",因为优化这些会谈/文章的重点适用于一组有限的案例.

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

与以往一样,当您不需要副本时,通过const引用可以节省大量复制.

现在来看具体的例子:

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

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

通过const引用的日子并没有结束 - 规则比以前更加复杂.如果性能很重要,那么根据您在实现中使用的详细信息,最好考虑如何传递这些类型.

  • 在堆栈使用方面,典型的ABI将在寄存器中传递单个引用而不使用堆栈. (2认同)

BЈо*_*вић 60

这在很大程度上取决于编译器的实现.

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

让我们考虑下一个功能:

bool foo1( const std::string v )
{
  return v.empty();
}
bool foo2( const std::string & v )
{
  return v.empty();
}
Run Code Online (Sandbox Code Playgroud)

这些函数在单独的编译单元中实现,以避免内联.然后:
1.如果你将一个文字传递给这两个函数,你就不会看到很多不同的表现.在这两种情况下,都必须创建一个字符串对象
2.如果传递另一个std :: string对象,foo2则会胜过foo1,因为它foo1会执行深层复制.

在我的电脑上,使用g ++ 4.6.1,我得到了以下结果:

  • 通过引用变量:1000000000次迭代 - >经过的时间:2.25912秒
  • 按值变化:1000000000次迭代 - >经过的时间:27.2259秒
  • 通过引用的字面值:100000000次迭代 - >经过的时间:9.10319秒
  • 按字面值计算:100000000次迭代 - >经过的时间:8.62659秒

  • 那不是我的观点.是否通过值或通过引用传递更好取决于您在函数内部执行的操作.在您的示例中,您实际上并没有使用太多的字符串对象,因此引用显然更好.但是,如果函数的任务是将字符串放在某个结构中或者执行一些涉及字符串多次拆分的递归算法,那么与通过引用传递相比,传递值可能实际上是_save_一些复制.Nicol Bolas很好地解释了这一点. (5认同)
  • 更有意义的是发生了什么_inside_函数:_如果用引用调用它,它需要在内部进行复制,在传递value_时可以省略吗? (4认同)
  • 对我来说"这取决于你在函数内部做了什么"是糟糕的设计 - 因为你是在实现的内部基础上建立函数的签名. (2认同)
  • 可能是一个拼写错误,但最后两个字面计时的循环次数减少了 10 倍。 (2认同)

Cod*_*gry 49

简答:不!答案很长:

  • 如果您不修改字符串(处理为只读),请将其作为传递const ref&.
    (const ref&当使用它的函数执行时,显然需要保持在范围内)
  • 如果您打算修改它或者您知道它将超出范围(线程),请将其作为a传递value,不要复制const ref&函数体内部.

cpp-next.com上有一篇名为"想要速度,按值传递!"的帖子..TL; DR:

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

翻译^

不要复制函数参数 ---意味着:如果计划通过将参数值复制到内部变量来修改参数值,则只需使用值参数.

所以,不要这样做:

std::string function(const std::string& aString){
    auto vString(aString);
    vString.clear();
    return vString;
}
Run Code Online (Sandbox Code Playgroud)

这样做:

std::string function(std::string aString){
    aString.clear();
    return aString;
}
Run Code Online (Sandbox Code Playgroud)

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

您只需要知道您打算如何在函数体中使用该参数.只读或不...并且如果它在范围内.

  • 对于是否通过价值或通过参考传递问题,我不会偏袒任何一方.我的观点是,你提倡在某些情况下通过引用传递,但随后引用(似乎支持你的立场)一个建议总是按值传递的指南.如果您不同意该指南,您可能想这样说并解释原因.(指向cpp-next.com的链接对我不起作用.) (11认同)
  • @KeithThompson**指南**引用*(不要复制你的函数参数.而是按值传递它们,让编译器进行复制.)*从该页面复制.如果这还不够清楚,我无能为力.我并不完全信任编译器做出最佳选择.在定义函数参数的方式中,我宁愿非常清楚我的意图.#1如果它是只读的,那就是`const ref&`.#2如果我需要写它或者我知道它超出范围......我使用一个值.#3如果我需要修改原始值,我会通过`ref&`传递.#4如果参数是可选的,我使用`pointers*`,所以我可以`nullptr`它. (5认同)
  • @KeithThompson:你错误地解释了指南.它不是"永远"通过价值.总而言之,它是"如果您已经制作了本地副本,请使用按值传递让编译器为您执行该副本." 当你不打算复制时,它并没有说使用pass-by-value. (4认同)
  • @KeithThompson**不要复制你的函数参数.**表示不要将`const ref&`复制到内部变量来修改它.如果需要修改它...将参数设为值.对于我的非英语自我来说,这是相当清楚的. (3认同)
  • 您建议在某些情况下通过引用传递,但是您指向建议始​​终按值传递的指南. (2认同)

bam*_*s53 43

除非你真的需要副本,否则它仍然是合理的const &.例如:

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

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

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


简而言之,除了那些不信任copy elision的人之外,C++ 11在这方面并没有真正改变.

  • 移动构造函数通常使用noexcept实现,但复制构造函数显然不是。 (2认同)

Yak*_*ont 24

几乎.

在C++ 17中,我们basic_string_view<?>将基本上用于std::string const&参数的一个狭窄用例.

移动语义的存在已经消除了一个用例std::string const&- 如果你计划存储参数,那么取一个std::stringby值就更合理了,因为你可以move超出参数.

如果有人用原始C调用你的函数,"string"这意味着只std::string分配了一个缓冲区,而不是两个缓冲区std::string const&.

但是,如果您不打算复制,那么std::string const&在C++ 14中使用by 仍然很有用.

使用std::string_view,只要您没有将所述字符串传递给期望C样式'\0'终止字符缓冲区的API ,您就可以更高效地获得std::string类似功能,而不会冒任何分配风险.原始C字符串甚至可以变成std::string_view没有任何分配或字符复制的字符串.

在这一点上,std::string const&当你没有复制批量数据,并且将它传递给期望空终止缓冲区的C风格的API时,你需要使用提供的更高级别的字符串函数std::string.实际上,这是一组罕见的要求.

  • 我很欣赏这个答案——但我确实想指出它确实受到了一些特定领域偏见的影响(就像许多高质量的答案一样)。即:“在实践中,这是一组罕见的要求”……在我自己的开发经验中,这些限制——对作者来说似乎异常狭窄——几乎一直都在满足。值得指出这一点。 (2认同)
  • @fish2000“‘原始 C 字符串甚至可以转换为 std::string_view,无需任何分配或字符复制’,这是值得记住的”。确实如此,但它遗漏了最好的部分——在原始字符串是字符串文字的情况下,*它甚至不需要运行时 strlen()*! (2认同)

Pup*_*ppy 17

std::string不是普通旧数据(POD),它的原始大小并不是最相关的东西.例如,如果传入一个高于SSO长度并在堆上分配的字符串,我希望复制构造函数不复制SSO存储.

建议这样做的原因是因为它inval是从参数表达式构造的,因此总是在适当时移动或复制 - 假设您需要参数的所有权,则不会有性能损失.如果不这样做,const参考仍然是更好的方法.

  • @Benj:旧的评论我知道,但如果SSO足够小,那么无条件地复制它比进行条件分支更快.例如,64字节是一个缓存行,可以在非常微不足道的时间内复制.在x86_64上可能是8个周期或更少. (3认同)
  • 关于复制构造函数足够聪明的有趣观点,如果不使用它,不要担心SSO.可能是正确的,我将不得不检查是否真的;-) (2认同)

How*_*ant 16

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

以下是衡量要求的代码:

#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());
}
Run Code Online (Sandbox Code Playgroud)

对我来说这个输出:

$ 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&&)
Run Code Online (Sandbox Code Playgroud)

下表总结了我的结果(使用clang -std = c ++ 11).第一个数字是复制结构的数量,第二个数字是移动结构的数量:

+----+--------+--------+---------+
|    | lvalue | xvalue | prvalue |
+----+--------+--------+---------+
| p1 |  1/1   |  0/2   |   0/1   |
+----+--------+--------+---------+
| p2 |  1/0   |  0/1   |   0/1   |
+----+--------+--------+---------+
Run Code Online (Sandbox Code Playgroud)

按值传递解决方案只需要一次重载,但在传递左值和x值时需要额外的移动构造.对于任何给定的情况,这可能是也可能是不可接受的.两种解决方案都有优点和缺点.

  • 这个答案计算了一个std :: string在Herb和Dave描述的pass-by-value设计下将经历的移动和复制的数量,以及通过引用传递一对重载函数.我在演示中使用了OP的代码,除了在复制/移动时用虚拟字符串代替喊出来. (3认同)
  • @TheParamagneticCroissant:你有不同的结果吗?如果是这样,使用什么编译器与什么命令行参数? (3认同)

cir*_*314 14

Herb Sutter与Bja​​rne 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
}
Run Code Online (Sandbox Code Playgroud)

OUTPUT:

const char * constructor
const std::string& constructor

Second string
Second string
Run Code Online (Sandbox Code Playgroud)


dig*_*ity 7

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

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

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


JDł*_*osz 6

请参阅"Herb Sutter"回到基础知识!现代C++风格的基本知识".在其他主题中,他回顾了过去给出的参数传递建议,以及C++ 11带来的新想法,特别是按值传递字符串的想法.

幻灯片24

基准测试显示std::string,在函数将以任何方式复制它的情况下,按值传递s可能会明显变慢!

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

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

它仅适用于"sink"参数,其中必须创建字符串(不更改其现有值),即按值传递技巧有效.也就是说,参数直接初始化匹配类型的成员的构造函数.

如果你想看看你有多深入担心这一点,请观看Nicolai Josuttis的演讲并祝你好运("完美 - 完成!"在找到上一版本的错误后n次.曾经去过那里?)


这也归纳为标准指南中的⧺F.15.