在C ++ 11中返回本地值的最佳方法

时间:2013-08-26 15:12:42

标签: c++11 copy move-semantics return-by-reference return-by-value

在过去,如果我想要一个对象A的字符串表示,我会写一些带有签名void to_string(const A& a, string& out)的东西,以避免额外的副本。这仍然是C ++ 11中的最佳实践,具有移动语义和所有?

我已经阅读了一些关于其他上下文的评论,这些评论建议依赖于RVO,而是编写string to_string(const A& a)。但RVO并不能保证会发生!那么,作为to_string的程序员,我怎么能保证不会不必要地复制字符串(独立于编译器)?

3 个答案:

答案 0 :(得分:6)

假设你的函数中的代码是以下形式:

std::string data = ...;
//do some processing.
return data;

如果elision不可用,则需要调用std::string的移动构造函数。最糟糕的情况是,你可以从你的内部字符串中移动。

如果您负担不起移动操作的费用,那么您必须将其作为参考传递。

话虽如此......你是否担心编译器无法内联短函数?您是否担心小包装是否会被无法正确优化?编译器不能优化for循环等的可能性是否打扰了你?您是否考虑if(x < y)是否比if(x - y < 0)更快?

如果不是......那你为什么要关心复制/移动省略(“返回值优化”的技术术语,因为它在更多地方使用)?如果您使用的是不支持复制省略的编译器,那么您使用的是可怕的编译器,可能无法支持大量其他优化。出于性能考虑,最好将时间花在升级编译器上,而不是将返回值转换为引用。

  防止副本实际发生的不可能的情况不值得......麻烦?代码不太可读?到底是什么?在简单回报方面加权的额外因素是什么?

“额外的事情”就是:

std::string aString = to_string(a);

比这更具可读性:

std::string aString;
to_string(a, aString);

在第一种情况下,立即显然to_string正在初始化字符串。在第二个,它不是;您必须查找to_string的签名才能看到它正在使用const 参考。

第一种情况甚至不是“惯用的”;这就是每个人通常会写它的方式。你永远不会看到to_int(a, someInt)整数调用;这是荒谬的。为什么整数创建和对象创建如此不同?作为程序员,你不应该不得不关心是否有太多的副本发生了返回值或其他事情。你只需要做简单,明显和易于理解的方式。

答案 1 :(得分:4)

在过去(1970-1980),您可以通过计算浮点除法来预测算法的性能。

今天不再如此。但是,您可以使用类似的规则来估算当前的效果:

  

计算到堆的行程数:new/mallocdelete/free

假设:

std::string
to_string(const A& a)
{
    std::string s;
    // fill it up
    return s;
}

std::string s = test();

假设您没有将s内部重新分配到to_string(),我会计为1。在将数据放入s时完成一次分配。我知道std::string有一个快速(无分配)移动构造函数。因此,RVO是否发生与估计to_string()的性能无关。在s之外创建to_string()时,将有1个分配。

现在考虑:

void
to_string(const A& a, string& out)
{
    out = ...
}

std::string s;
to_string(a, s);

正如我所写,它仍然消耗1个内存分配。因此,这与返回值版本的速度大致相同。

现在考虑一个新的用例:

while (i_need_to)
{
    std::string s = to_string(get_A());
    process(s);
    update(i_need_to);
}

根据我们之前的分析,上面每次迭代将进行1次分配。现在考虑一下:

std::string s;
while (i_need_to)
{
    to_string(get_A(), s);
    process(s);
    update(i_need_to);
}

我知道stringcapacity(),并且该容量可以通过上述循环中的多种用途进行回收。最糟糕的情况是每次迭代我仍然有1个分配。最好的情况是第一次迭代将创建足够大的容量来处理所有其他迭代,并且整个循环只会进行1次分配。

事实可能介于最糟糕和最佳情况之间。

最好的API将取决于您认为您的功能最有可能的用例。

计算分配以估算效果。然后衡量你编码的内容。在std::string的情况下,可能会有一个短字符串缓冲区,可能会影响您的决定。对于libc++,在64位平台上,std::string将在到达堆之前存储最多22个char(加上终止空值)。

答案 2 :(得分:0)

以下是我从反馈和其他资源中收集到的答案:

按价值直接回归是成语,因为:

  • 在练习中,大部分时间都会进行复制/移动操作;
  • 移动ctor将用于后备;
  • 防止副本实际发生的不可能的情况不值得用不那么易读的代码
  • 传入引用需要已经创建了对象
    • 并不总是可行(例如,可能没有默认的ctor)以及
    • 如果问题是性能,则必须考虑太多初始化

但是,如果预计典型用法类似于

std::string s;
while (i_need_to)
{
    to_string(get_A(), s);
    process(s);
    update(i_need_to);
}

如果有问题的类型有默认构造函数*,那么传递应该通过引用返回的对象仍然是有意义的。

*仅考虑字符串作为示例,但问题和答案可以概括