2

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

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

4

3 回答 3

6

假设您的函数中的代码采用以下形式:

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

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)整数的调用;这是荒谬的。为什么整数创建和对象创建如此不同?作为一个程序员,你不应该关心是否有太多的副本是为了返回值或其他东西发生的。您只需以简单、明显和易于理解的方式做事。

于 2013-08-26T15:46:01.920 回答
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(). 当您将数据放入s. 我知道它std::string有一个快速(无分配)的移动构造函数。所以 RVO 是否发生与估计to_string(). 在创建s外部时将有 1 个分配to_string()

现在考虑:

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

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

真相可能介于最坏和最好的情况之间。

最好的 API 取决于你认为你的函数最有可能出现的用例。

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

于 2013-08-26T23:53:20.220 回答
0

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

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

  • 在实践中,大部分时间都会发生复制/移动省略;
  • move ctor 将用于回退;
  • 防止实际发生的不太可能的复制情况不值得使用可读性较差的代码
  • 传入引用要求对象已经创建
    • 并不总是可行的(例如,可能没有默认 ctor)并且
    • 如果问题是性能,还必须考虑一个初始化太多

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

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

如果所讨论的类型具有默认构造函数*,那么传递应该通过引用保存返回的对象可能仍然有意义。

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

于 2013-08-26T17:48:22.367 回答