引用还是返回 - 最佳实践

7
例如,我们有一个编码函数。最佳实践是如何使用:
void Crypto::encoding(string &input, string &output)
{
    //encoding string
    output = encoded_string;
}

或者

string Crypto::encoding(string &input)
{
    //encoding string
    return encoded_string;
}

我们在返回字符串时应该使用引用还是返回值?据我所知,返回一个字符串需要一些时间来初始化一个将由返回指令返回的新字符串。当使用引用变量时,我不浪费时间初始化一些新变量,只需结束函数即可。

我们应该大多数情况下使用引用并将函数返回类型设置为void吗?还是只有在返回两个或更多变量以及需要返回一个变量时才使用返回指令?


请在编辑器中使用 {} 按钮来格式化代码。 - Mat
7
你真的担心你所提到的“时间”吗?你是否进行了分析,并确定字符串构建时间是你的应用程序瓶颈? - Kerrek SB
7个回答

10

不要优化你没有测量的东西。

通常最好(更易读)使用return返回计算结果。如果由于对象太大而需要耗费太长时间,则仍可以通过引用参数返回结果,但前提是您已经证明这将显著提高性能(进行测量)。例如,如果您只编码非常短的字符串,并且只偶尔这样做,则复制的开销可以忽略不计。


3
+1 for the 不要优化你没有测量过的东西,Scott Meyers 关于优化的一句话非常贴切,“你需要确定那些运行时间占用90%的代码中的前20%,然后尝试对这20%的代码进行优化。”我记不清具体措辞了,但是意思是相同的。 - Alok Save
1
我以为是10%,不是20% :-) - rve
2
说到测量,在g++ 4.3上,这两个函数的运行速度相同。http://ideone.com/sgl9W - Robᵩ

7

由于大多数现代编译器都具有RVO功能,因此通常可以消除复制操作。即使没有使用c++11,您也可以获得这些好处。


3
如果您的编译器支持C++11标准和r-value references,那么通过值返回std::string实际上是非常高效的。在此功能出现之前,答案可能会有所不同,因为您只能依靠编译器执行RVO
我认为使用返回值可能更自然,也意味着您可以将结果分配给一个常量局部变量或类成员,以避免意外修改,例如:
const std::string result = crypo.encoding("blah");

或者

class SomeClass
{
public:
    Someclass(Crypto& crypto, const std::string& input) :
        m_output(crypo.encoding(input))
    {
    }

private:
    const std::string m_output;
};

请确保不要返回const值,否则将抑制移动语义。


2
我使用引用。这使得实现者可以进行抽象和选择,而不会过度负担客户(有些情况很重要,有些情况则不是)。
我还使用它们来保持一致的风格——我不喜欢看到公共接口通过细节来传递其实现。
瞬态和副本可能很昂贵——这取决于您正在传递的类型。按值返回表示该类型应该是可平凡构造、可交换、可复制、可移动的。编译器在这个领域可以做出一些很好的优化(RVO/移动),但您也可以做出明智的决策,以最小化实现中的昂贵操作。一旦您不再使用每个人都知道复制特性的类型,那么选择如何返回就变得非常复杂,因此我只保持简单并支持引用。
传递引用还有一些其他好处,例如当客户端希望使用传递的类型的子类时。
如果需要优化的程序:我经常删除复制构造函数和operator=,如果它们不是平凡的或可能的。通过可变引用传递允许您使用不可复制/可分配的类型。
在这个问题中使用的std::string的严格范围内:按值返回std::string是非常普遍的,而且已经专门为这种情况进行了许多优化——RVO、COW和移动是其中一些显著的优化。正如Voo在下面的评论中提到的那样,按值返回通常更容易阅读。对于std::string和更高级别的程序,按值返回不太可能成为一个问题,但是重要的是要测量,以便了解您正在使用的标准库实现所涉及的成本(您的问题表明性能可能很重要)。
一个重要的考虑因素是,如果您想改进现有程序,请确保您了解实现的执行方式,并学习如何在性能重要时最有效地使用类型。实现可能是针对实际使用编写和优化的,这意味着它们可能在某些情况下持悲观态度并猜测您,在某些情况下,您尝试提高性能的尝试可能已经实现或使用类型的非传统用法可能会降低性能。典型的std::vector的调整大小行为就是一个明显的例子。走高性能道路确实需要很多时间和复杂性,关于您需要了解以获得最佳结果的内容,这显然因您使用的实现和您使用的类型而异。如果性能很重要且值得投入非平凡的时间,那么学习您使用的类型的操作是一项值得进行的努力,可以带来显著的收益。

我还应该补充一点,我经常在低层级别工作——性能至关重要和/或资源有限。可能会有许多限制,包括没有异常、没有锁(也意味着没有堆分配)、最小的抽象成本,甚至受到动态多态性的限制。即使对于C++来说,这可以被认为是一个相当苛刻的领域。我选择引用核心低级部件,但如果我知道程序仅在更高级别的领域或单元测试中使用,我将放宽这个规则。


@anonymous_downvoter,这并没有太大的帮助,除非您能够证明您的行为是有道理的。 - justin
1
虽然我认为只返回对象会导致更简单、易读的代码,但你在这里提出了一个合理的论点,所以+1——能够摆脱复制构造函数和operator=确实很好,我也很想经常这样做...不过没有完美的东西。 - Voo
@Voo 是的,我同意返回值通常更容易阅读(特别是当类型像 std::string 这样简单时)。现在我已经重新阅读了这个答案,我将添加一些细节/背景。干杯。 - justin

1
我要记录一下:可能都不是。
在我看来,你的 `encode` 很像一个通用算法,应该真正使用迭代器而不是直接处理字符串。
template <class InputIterator, class OutputIterator>
void encode(InputIterator begin, InputIterator end, OutputIterator result) {
    while (begin!=end)
        *result++ = encode_byte(*begin++);
}

这样,您可以轻松地重复使用完全相同的代码,从输入流(通过std :: istream_iterator )直接编码数据到输出流(通过std :: ostream_iterator ),如此方便。

这通常还消除了大多数关于效率的问题。


只是好奇:如果我将 Sammich::Iterator 传递给你的函数,那么 encode_byte() 如何知道如何编码三明治?您还需要提供编码算法才能真正实现通用性,但从您的函数中剩下的只有迭代,因此它应该被命名为 for_each...;-) - EricSchaefer
@EricSchaefer:显然,它不能自动获得如何编码每种可能的输入类型的特殊知识。但是,它可以(大多数情况下)独立于容纳数据的容器类型。 - Jerry Coffin
你描述的不就是 std::transform 吗? - user213313
@BleepBloop:是的和不是。根据所涉及的编码方式,transform可能确实是一个好选择。另一方面,transform期望从输入元素到输出元素的精确1:1映射。这将很难(至少)应用于像将UTF-8转换为UCS-4(或反之亦然)这样的情况,其中输入和输出的数量不同。 - Jerry Coffin
但是你的函数模板除了调用一个固定的操作而不是作为参数之外,完全相同。在这里你仍然做着相同的1:1映射 - 每次迭代都需要写入一个新的输出元素(result++)和消耗一个新的输入元素(begin++)。 - user213313
@BleepBloop:是的,上面的演示代码没有显示任何差异。但那只是演示代码。当/如果你写真正的代码时,如果适合的话,你会使用transform,否则你会自己编写其他内容。 - Jerry Coffin

1

使用新标准C++11,由于新的移动语义,您可以使用第二种变体。

然而,很可能您的编译器仍然只支持旧标准。在这种情况下,您的第一个示例不会引起任何复制,并且更好。


听起来像是微调优。 - BЈовић
1
@VJo:并不完全如此,如果你的字符串有数百万个字符,或者你调用这个函数数百万次,那么在第一个示例中你只需要触及一半的内存,而在第二个示例中则需要触及全部内存。这也是移动语义被引入的原因之一。 - Manuel
4
第二个可能也不涉及复制,参见RVO/NRVO或https://dev59.com/JnM_5IYBdhLWcg3wZSTX。 - rve
除非进行深拷贝(在本例中并非如此),否则这是一种微观优化。 - BЈовић

1

我更喜欢第二个版本,因为它看起来更像一个数学函数。如果你只返回字符串,那么在性能方面应该是不错的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接