如何避免复制构造返回值

时间:2015-07-24 04:04:33

标签: c++ reference return return-value

我是C ++的新手,我最近遇到了一个问题,即返回对局部变量的引用。我通过将返回值从std::string&更改为std::string来解决此问题。但是,根据我的理解,这可能是非常低效的。请考虑以下代码:

string hello()
{
    string result = "hello";
    return result;
}

int main()
{
    string greeting = hello();
}

根据我的理解,会发生什么:

  • hello()被召唤。
  • 为本地变量result分配值"hello"
  • result的值会复制到变量greeting

这对std::string来说可能并不重要,但如果你有一个包含数百个条目的哈希表,它肯定会变得昂贵。

如何避免复制构造返回的临时文件,而是返回指向对象的指针副本(实质上是本地变量的副本)?

旁注:我heard编译器有时会执行返回值优化以避免调用复制构造函数,但我认为最好不要依赖编译器优化来使代码高效运行。)< / p>

3 个答案:

答案 0 :(得分:12)

您问题中的描述非常正确。但重要的是要理解这是抽象C ++机器的行为。事实上,抽象回归行为的规范描述甚至不太理想

  1. result被复制到std::string类型的无名中间临时对象中。该函数返回后暂时存在。
  2. 然后在函数返回后将无名的中间临时对象复制到greeting
  3. 大多数编译器总是足够智能,完全按照经典的复制省略规则消除中间临时版。但即使没有那种中间暂时性,这种行为也一直被认为是非常不理想的。这就是为什么给编译器提供了很多自由,以便为它们提供价值回报的优化机会。最初是返回值优化(RVO)。后来添加了命名返回值优化(NRVO)。最后,在C ++ 11中,移动语义成为在这种情况下优化返回行为的另一种方法。

    请注意,在您的示例中,在NRVO下,result"hello"的初始化实际上会将"hello"从一开始直接放入greeting

    所以在现代C ++中,最好的建议是:保持原样并且不要避免它。按价值归还。 (并且无论何时可以在声明点使用立即初始化,而不是选择默认初始化然后进行赋值。)

    首先,编译器的RVO / NRVO功能可以(并且将)消除复制。在任何自尊的编译器中,RVO / NRVO都不是模糊的或次要的。这是编译器编写者积极努力实现和实现的东西。

    其次,如果RVO / NRVO以某种方式失败或不适用,则始终将语义作为后备解决方案。移动自然适用于按值返回的上下文,并且比非平凡对象的完全复制便宜得多。 std::string是一种可移动的类型。

答案 1 :(得分:5)

我不同意这句话“我认为最好不依赖编译器优化来使代码高效运行。”这基本上就是编译器的整个工作。您的工作是编写清晰,正确且可维护的源代码。对于我曾经不得不修复的每个性能问题,我必须修复由开发人员试图变得聪明而不是做一些简单,正确和可维护的事情引起的一百多个问题。

让我们来看看你可以做些什么来尝试“帮助”编译器,看看它们如何影响源代码的可维护性。

  • 您可以通过参考
  • 返回数据

例如:

void hello(std::string& outString)

使用引用返回数据会使调用站点的代码难以阅读。几乎不可能告诉哪些函数调用mutate state作为副作用,哪些不作为副作用。即使你非常小心使用const限定引用,它也很难在通话网站上阅读。请考虑以下示例:

void hello(std::string& outString); //<-This one could modify outString
void out(const std::string& toWrite); //<-This one definitely doesn't.

. . .

std::string myString;
hello(myString); //<-This one maybe mutates myString - hard to tell.
out(myString);   //<-This one certainly doesn't, but it looks identical to the one above

即使是你好的声明也不清楚。它是修改outString,还是作者只是马虎而忘了const限定引用?用functional style编写的代码更容易阅读和理解,更难以意外破解。

避免通过参考

返回数据
  • 您可以返回指向对象的指针,而不是返回对象。

返回指向对象的指针使得很难确定您的代码是否正确。除非你使用unique_ptr,否则你必须相信任何使用你方法的人都是彻底的,并确保在完成它时删除指针,但这不是很RAII。 std :: string已经是char *的一种RAII包装器,它抽象出与返回指针相关的数据生命周期问题。返回指向std :: string的指针只是重新引入了std :: string旨在解决的问题。依靠人类勤奋并仔细阅读您的函数文档,知道何时删除指针以及何时不删除指针不太可能产生积极的结果。

避免返回指向对象的指针而不是返回对象

  • 这让我们移动了构造函数。

移动构造函数只会将指向数据的所有权从“结果”转移到其最终目标。之后,访问“结果”对象无效,但这无关紧要 - 您的方法已结束且“结果”对象超出范围。没有副本,只是用明确的语义转移指针的所有权。

通常,编译器会为您调用move构造函数。如果你真的是偏执狂(或者具有编译器无法帮助你的特定知识),你可以使用std::move

尽可能使用移动构造函数

最后现代编译器非常棒。使用现代C ++编译器,99%的时间编译器将进行某种优化以消除副本。另外1%的时间可能不会影响性能。在特定情况下,编译器可以重写一个方法,如std :: string GetString();取消GetString(std :: string&amp; outVar);自动。代码仍然易于阅读,但在最终的程序集中,您可以获得通过引用返回的所有真实或想象的速度优势。除非您具备解决方案不符合业务要求的特定知识,否则不要牺牲性能的可读性和可维护性。

答案 2 :(得分:3)

有很多方法可以实现这一目标:

1)通过参考

返回一些数据
void SomeFunc(std::string& sResult)
{
  sResult = "Hello world!";
}

2)返回指向对象的指针

CSomeHugeClass* SomeFunc()
{
  CSomeHugeClass* pPtr = new CSomeHugeClass();
  //...
  return(pPtr);
}

3)在这种情况下,C ++ 11可以使用移动构造函数。有关其他信息,请参阅this thisthis