从函数中返回对象

19

我现在非常困惑,不知道该如何以及使用哪种方法从函数中返回对象。我希望能够针对给定要求的解决方案得到一些反馈。

情景A: 返回的对象将被存储在变量中,在其生命周期内无需修改。因此,

const Foo SomeClass::GetFoo() {
 return Foo(); 
}

被调用为:

someMethod() {
 const Foo& l_Foo = someClassPInstance->GetFoo();
//...
}

场景 B: 返回的对象将被存储在一个变量中,而这个变量在其生命周期内将被修改。因此,

void SomeClass::GetFoo(Foo& a_Foo_ref) {
     a_Foo_ref = Foo(); 
    }

被调用为:

someMethod() {
 Foo l_Foo;
 someClassPInstance->GetFoo(l_Foo);
//...
}

我有一个问题:假设Foo不能有默认构造函数,在这种情况下,你会如何处理,因为我们不能再写这样的代码了:

Foo l_Foo

场景C:

Foo SomeClass::GetFoo() {
 return Foo(); 
}

调用方式:

someMethod() {
 Foo l_Foo = someClassPInstance->GetFoo();
//...
}

我认为这不是推荐的方法,因为它会导致构造额外的临时对象。

你认为呢?另外,你有没有更好的处理方法?


2
http://en.wikipedia.org/wiki/Return_value_optimization - vladr
方案A虽然可以,但与C相比并没有什么改进。除非Foo的默认构造函数很“快”,Foo的复制构造函数很“慢”,而且您不想依赖编译器足够聪明地省略不必要的拷贝,否则我会使用C。在这种情况下,方案B也是可以接受的。 - sellibitze
请注意,a_Foo_ref = Foo(); 创建对象并执行复制,几乎等同于最后一种情况。 - Anycorn
@aaa:不,它不能。该语句只允许构造一个对象。 - Billy ONeal
1
是的,在这种情况下仍然适用。RVO的工作方式是,调用代码为对象分配内存,然后将该内存的地址传递给函数,函数在该内存中构造返回的对象。在您的示例中调用add()函数不会干扰它。但是,可能导致RVO不起作用的一件事是当函数创建多个对象并可能返回其中任何一个时,如此处所示:http://en.wikipedia.org/wiki/Return_value_optimization#Compiler_support - Josh Townzen
显示剩余2条评论
2个回答

16

首先,让我们看看在这里起作用的因素:

(a) 当一个临时变量被用来初始化一个引用时,如何延长其寿命 - 我在这篇文章中学到了这个知识点,作者是Andrei Anexandrescu。虽然感觉有些奇怪,但确实很有用:

class Foo { ... }

Foo GetFoo() { return Foo(); }  // returning temporary

void UseGetFoo()
{
   Foo const & foo = GetFoo();
   // ... rock'n'roll ...
   foo.StillHere();
}

规则规定,当引用使用临时变量初始化时,临时变量的生命周期将延长到引用超出范围。(此回复引用了正典)

(b) 返回值优化 - (维基百科) - 在某些情况下,本地的两个副本 --> 返回值 --> 本地 可能被省略。这是一个令人惊讶的规则,因为它允许编译器改变可观察行为,但也很有用。

就是这样。 C++ - 奇怪但有用。


因此,看看您的场景

场景A:您正在返回一个临时变量,并将其绑定到引用-临时变量的生命周期延长到l_Foo的生命周期。

请注意,如果GetFoo返回引用而不是临时变量,则无法使用此方法。

场景B:可以工作,但它强制实现构造-构造-复制循环(可能比单个构造更昂贵),以及您提到需要默认构造函数的问题。

我不会使用该模式来创建对象-仅用于改变现有对象。

场景C:编译器可以省略临时副本(根据RVO规则)。不幸的是没有保证-但是现代编译器确实实现了RVO。

C++ 0x中的右值引用允许Foo实现资源窃取构造函数,不仅保证抑制副本,而且在其他情况下也非常方便。

(我怀疑是否有实现rvalue引用但不实现RVO的编译器。但是有些情况下RVO无法启动。)


像这样的问题需要提到智能指针,例如shared_ptrunique_ptr(后者是“安全”的auto_ptr)。它们也在C++ 0x中。它们为创建对象的函数提供了一种替代模式。



@peter:感谢您提供如此详细的回复和链接!我非常感激。我一直在尝试解决这个问题,所以对A情况很熟悉。我对B情况有一个问题。当我需要修改对象而不是在GetFoo()内创建对象时,我通常使用B。现在,假设我们调用Foo l_Foo; someClassPInstance->GetFoo(l_Foo);:: 我们发送l_Foo的引用,因此这里没有临时变量。然后,在void SomeClass::GetFoo(Foo& a_Foo_ref){ //.. }中,假设我只是修改它而不是创建它。您认为B是一个好选择吗? - brainydexter
另外,请查看我上一个评论中的原始问题。 - brainydexter
@brainydexter:是的,这是引用的典型使用场景。它需要一些文档(是in还是out等)。谢谢提供参考,我会更新链接的。---最近我也尝试了rvalue引用——对我来说是一个很好的练习。 - peterchen
"Foo const & f = GetFoo();"应该更改为"Foo const & foo = GetFoo();", 但是答案依然很好。 - Cory-G

4
在这三种情况中,第3种是惯用方法,也是你应该使用的方法。如果可能,编译器可以自由使用拷贝省略来避免额外的拷贝开销。 情况A是错误的。你最终会得到一个对临时变量的引用,当包含函数调用的语句结束时,该临时变量将被销毁。好吧,情况A并不是错误的,但你仍然应该使用情况C。
情况B完全正常,但是:

假设Foo没有默认构造函数。那么在这种情况下你该怎么处理呢?因为我们不能再写:Foo l_Foo

Foo必须有一个默认构造函数。即使你没有给它一个,默认情况下编译器也必须为你提供一个。假设你声明了一个private构造函数,那么你无法使用这种调用方法。你需要使Foo具有默认构造函数或使用情况C。

1
const Foo SomeClass::GetFoo() :: 场景A :: 我这里不是返回临时对象的引用。 - brainydexter
@brainydexter:糟糕..你是正确的。然而,我认为它仍然是错误的。我不是100%确定,但我相信即使有一个引用,临时对象也会被销毁。你需要拥有实际的对象。 - Billy ONeal
1
方案A不错,但与C相比没有真正的改进。在C++中有一条特殊规则,可以延长临时对象的生命周期。 - sellibitze
你要求的 :-) 我之前问过这个问题,它是有效的。你可以在这里阅读相关内容: https://dev59.com/IE3Sa4cB1Zd3GeqPsRpOhttp://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/ - brainydexter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接