【发布时间】:2013-08-14 16:47:47
【问题描述】:
我正在阅读有关 NRVO 的信息,并试图了解什么时候应该依赖它,什么时候不应该依赖它。现在我有一个问题:为什么要完全依赖 NRVO?总是可以通过引用显式传递返回参数,那么有什么理由改用 NRVO 吗?
【问题讨论】:
-
因为有时需要返回一个值,或者复制一个函数参数。
我正在阅读有关 NRVO 的信息,并试图了解什么时候应该依赖它,什么时候不应该依赖它。现在我有一个问题:为什么要完全依赖 NRVO?总是可以通过引用显式传递返回参数,那么有什么理由改用 NRVO 吗?
【问题讨论】:
处理返回值比处理通过写入引用参数返回的方法更容易。考虑以下两种方法
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);
【讨论】:
typedef std::vector<std::string> vector; vector readlines(); void readlines(vector*); 第一个告诉你它创建了一个带有读取行的向量。第二个不是很清楚,是不是clear()这个向量?是append吗?
这不是一个答案,但在某种意义上也是一个答案……
给定一个通过指针获取参数的函数,有一个简单的转换将产生一个按值返回的函数,并且编译器可以轻松优化。
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 都具有按值函数返回的共同特征:对于 large(不适合寄存器)返回类型,返回对象的内存由调用者分配,函数需要额外的带有该内存位置的指针。那是编译器看到的时候
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 来优化按值返回的临时对象。
最后,在许多情况下,按值返回比传入要通过非常量引用(或指针)更改的参数更容易链接。
【讨论】: