如何避免复制构造返回值

Jam*_* Ko 7 c++ return reference return-value

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

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

int main()
{
    string greeting = hello();
}
Run Code Online (Sandbox Code Playgroud)

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

  • hello() 叫做.
  • 为局部变量result赋值"hello".
  • 值的值result被复制到变量中greeting.

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

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


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

AnT*_*AnT 12

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

  1. result被复制到类型的无名中间临时对象std::string.函数返回后暂时存在.
  2. 然后greeting在函数返回后将该无名中间临时对象复制到该对象.

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

需要注意的是NRVO下在你的榜样初始化result"hello"实际的地方是"hello"直接进入greeting从一开始.

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

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

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


Pet*_*man 5

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

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

  • 您可以通过引用返回数据

例如:

void hello(std::string& outString)
Run Code Online (Sandbox Code Playgroud)

使用引用返回数据会使调用站点的代码难以读取.几乎不可能告诉哪些函数调用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
Run Code Online (Sandbox Code Playgroud)

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

避免通过引用返回数据

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

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

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

  • 这使我们移动构造函数.

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

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

尽可能使用移动构造函数

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