我是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>
答案 0 :(得分:12)
您问题中的描述非常正确。但重要的是要理解这是抽象C ++机器的行为。事实上,抽象回归行为的规范描述甚至不太理想
result
被复制到std::string
类型的无名中间临时对象中。该函数返回后暂时存在。greeting
。大多数编译器总是足够智能,完全按照经典的复制省略规则消除中间临时版。但即使没有那种中间暂时性,这种行为也一直被认为是非常不理想的。这就是为什么给编译器提供了很多自由,以便为它们提供价值回报的优化机会。最初是返回值优化(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)