我正在阅读有关 NRVO 的信息,并试图了解什么时候应该依赖它,什么时候不应该依赖它。现在我有一个问题:为什么要完全依赖 NRVO?总是可以通过引用显式传递返回参数,那么有什么理由改用 NRVO 吗?
4 回答
处理返回值比处理通过写入引用参数返回的方法更容易。考虑以下2种方法
C GetByRet() { ... }
void GetByParam(C& returnValue) { ... }
第一个问题是无法链接方法调用
Method(GetByRet());
// vs.
C temp;
GetByParam(temp);
Method(temp);
它还使功能auto
无法使用。对于像这样的类型来说不是什么大问题,C
但对于像这样的类型来说更重要std::map<std::string, std::list<std::string>*>
auto ret = GetByRet();
// vs.
auto value; // Error!
GetByParam(value);
同样正如 GMacNickG 指出的那样,如果该类型C
有一个普通代码无法使用的私有构造函数怎么办?也许构造函数是private
或者只是没有默认构造函数。再次GetByRet
像冠军一样工作并且GetByParam
失败了
C ret = GetByRet(); // Score!
// vs.
C temp; // Error! Can't access the constructor
GetByParam(temp);
这不是一个答案,但在某种意义上它也是一个答案……
给定一个通过指针获取参数的函数,有一个简单的转换将产生一个按值返回的函数,并且编译器可以轻松优化。
void f(T *ptr) {
// uses ptr->...
}
在函数中添加对对象的引用,并将 ptr 的所有使用替换为引用
void f(T *ptr) { T & obj = *ptr; /* uses obj. instead of ptr-> */ }
现在删除参数,添加返回类型,替换
T& obj
为T obj
并将所有返回更改为 yield 'obj'T f() { T obj; // No longer a ref! /* code does not change */ return obj; }
此时,您有一个按值返回的函数,NRVO 是微不足道的,因为所有返回语句都引用同一个对象。
这个转换后的函数与指针传递有一些相同的缺点,但它从来没有比它更糟。但它表明,只要按指针传递是一个选项,按值返回也是一个具有相同成本的选项。
费用完全一样吗?
这超出了语言的范围,但是当编译器生成代码时,它会遵循 ABI(应用程序二进制接口),该 ABI(应用程序二进制接口)允许由不同运行的编译器(甚至同一平台中的不同编译器)构建的代码进行交互。所有当前使用的 ABI 都具有按值函数返回的共同特征:对于大型(不适合寄存器)返回类型,返回对象的内存由调用者分配,并且函数需要一个带有该内存位置的额外指针. 那是编译器看到的时候
T f();
调用约定将其转换为:
void mangled_name_for_f( T* __result )
因此,如果您比较备选方案:T t; f(&t);
并且T t = f();
在这两种情况下,生成的代码都会在调用者的框架中分配空间,[1],调用传递指针的函数。在函数结束时,编译器将 [2] 返回。其中 [#] 是在每个备选方案中实际调用对象构造函数的位置。两种选择的成本是相同的,不同之处在于,在 [1] 中,对象必须是默认构造的,而在 [2] 中,您可能已经知道对象的最终值,并且您可以做一些更有效的事情。
关于性能,仅此而已吗?
并不真地。如果您稍后需要将该对象传递给一个按值获取参数的函数,例如void g(T value)
,在按指针传递的情况下,调用者的堆栈中有一个命名对象,因此必须将对象复制(或移动)到调用约定需要 value 参数的位置。在按值返回的情况下,知道它会调用的编译器知道g(f())
返回的对象的唯一用途f()
是作为的参数g()
,所以它可以在调用时传递一个指向适当位置的指针f()
,这意味着赢了不要完成任何副本。在这一点上,手动方法开始真正落后于编译器的方法,即使实现f
使用上面的哑转换!
T obj; // default initialize
f(&obj); // assign (or modify in place)
g(obj); // copy
g(f()); // single object is returned and passed to g(), no copies
实际上不可能(或不希望)总是通过引用返回一个值(考虑operator+
作为一个基本的反例)。
回答您的问题:您通常不依赖或期望 NRVO 总是发生,但您确实希望编译器能够进行合理的优化工作。只有当分析表明复制返回值的成本很高时,您才需要担心帮助编译器提供提示或备用接口。
编辑some function could be optimized just by using return parameter
:
首先,请记住,如果不经常调用该函数,或者编译器具有足够的智能,则您不能保证 return-by-out-parameter 是一种优化。其次,请记住,您将拥有代码的未来维护者,并且编写清晰、易于理解的代码是您可以提供的最大帮助之一(无论代码损坏的速度有多快)。第三,花点时间阅读http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/看看它是否会改变你的想法。
许多人认为将非常量引用参数传递给函数,然后在函数中更改这些参数不是很直观。
此外,还有许多按值返回结果的预定义运算符(例如,算术运算符,如operator+
、operator-
等...)。由于您希望保留此类运算符的默认语义(和签名),因此您不得不依赖 NRVO 来优化按值返回的临时对象。
最后,在许多情况下,按值返回比传入要通过非常量引用(或指针)更改的参数更容易链接。