在函数中返回大对象

比较以下两段代码,第一段使用对大对象的引用,第二段使用大对象作为返回值。 强调“大对象”是指不必要地重复复制对象浪费周期。

使用对大对象的引用:

void getObjData( LargeObj& a ) { a.reset() ; a.fillWithData() ; } int main() { LargeObj a ; getObjData( a ) ; } 

使用大对象作为返回值:

 LargeObj getObjData() { LargeObj a ; a.fillWithData() ; return a ; } int main() { LargeObj a = getObjData() ; } 

代码的第一部分不需要复制大对象。

在第二个代码片段中,该对象是在函数内部创build的,所以通常在返回对象时需要一个副本。 然而,在这种情况下,在main()中声明了对象。 编译器是否会首先创build一个默认构造的对象,然后复制getObjData()返回的对象,还是会像第一个片段一样高效?

我认为第二个片段更容易阅读,但恐怕效率不高。

编辑:通常情况下,我正在考虑的情况LargeObj是generics容器类,为了参数的缘故,其中包含成千上万的对象。 例如,

 typedef std::vector<HugeObj> LargeObj ; 

所以直接修改/添加LargeObj方法不是一个直接可访问的解决scheme。

第二种方法比较习惯,performance力强。 阅读代码时,清楚的是函数对参数没有任何先决条件(它没有参数),并且实际上会在里面创build一个对象。 第一种方法对于不经意的读者来说并不是那么清楚。 这个调用意味着对象将被改变(通过引用传递),但是对于传递的对象是否有任何先决条件还不是很清楚。

关于副本。 您发布的代码不使用赋值运算符,而是使用复制构造。 C ++定义了在所有主要编译器中实现的返回值优化 。 如果您不确定可以在编译器中运行以下代码片段:

 #include <iostream> class X { public: X() { std::cout << "X::X()" << std::endl; } X( X const & ) { std::cout << "X::X( X const & )" << std::endl; } X& operator=( X const & ) { std::cout << "X::operator=(X const &)" << std::endl; } }; X f() { X tmp; return tmp; } int main() { X x = f(); } 

用g ++你将得到一行X :: X() 。 编译器为x对象保留堆栈空间,然后调用构造tmp over x的函数 (事实上tmp x f()内部的操作直接应用于x ,相当于第一个代码片段(通过参考)。

如果你没有使用复制构造函数(你写的是: x x; x = f(); ),那么它将创buildxtmp并应用赋值运算符,产生三行输出: X :: X() / X :: X() / X :: operator = 。 所以在这种情况下效率可能会有所下降。

使用第二种方法。 看起来效率不高,但C ++标准允许副本被回避。 这种优化被称为命名返回值优化,并在大多数当前编译器中实现。

是的,在第二种情况下,它将复制对象,可能两次 – 一次从函数返回值,再次将其分配给main中的本地副本。 一些编译器会优化第二个副本,但总的来说,你可以假设至less有一个副本会发生。

但是,即使对象中的数据很大,仍然可以使用第二种方法来清晰,而不会牺牲正确使用智能指针的性能。 在boost中检查一组智能指针类。 这样内部数据只被分配一次,从不复制,即使外部对象是。

避免任何复制的方法是提供一个特殊的构造函数。 如果您可以重新编写代码,如下所示:

 LargeObj getObjData() { return LargeObj( fillsomehow() ); } 

如果fillsomehow()返回数据(也许是一个“大string”,那么就有一个构造函数需要一个“大string”。如果你有这样一个构造函数,那么编译器会非常喜欢构造一个单一的对象,而不是完全拷贝当然,在现实生活中这是否是有用的取决于你的特殊问题。

一个有点惯用的解决scheme是:

 std::auto_ptr<LargeObj> getObjData() { std::auto_ptr<LargeObj> a(new LargeObj); a->fillWithData(); return a; } int main() { std::auto_ptr<LargeObj> a(getObjData()); } 

或者,您可以通过让对象获取自己的数据,即通过使getObjData()成为getObjData()成员函数来避免这个问题。 根据你实际在做什么,这可能是一个好方法。

根据对象的实际大小和操作的频率,不要因为效率太差而无法识别。 只有在确定是必要的时候才会发生优化,代价是干净可读的代码。

当您通过复印返回时,有些机会会浪费掉。 是否值得担心取决于对象的实际大小以及调用此代码的频率。

但是我想指出的是,如果LargeObj是一个大LargeObj平凡的类,那么在任何情况下,它的空构造函数都应该将它初始化为一个已知的状态:

 LargeObj::LargeObj() : m_member1(), m_member2(), ... {} 

这也浪费了几个周期。 重新编写代码

 LargeObj::LargeObj() { // (The body of fillWithData should ideally be re-written into // the initializer list...) fillWithData() ; } int main() { LargeObj a ; } 

对你来说可能是一个双赢:你可以将LargeObj实例初始化为已知的和有用的状态,并且你将有更less的浪费周期。

如果你不总是想在构造函数中使用fillWithData() ,你可以将一个标志作为parameter passing给构造函数。

更新 (从你的编辑和评论):语义上,如果值得为LargeObj创build一个typedef – 即给它一个名字,而不是简单地引用它作为typedef std::vector<HugeObj> – 那么你已经走上了赋予它自己的行为语义的道路。 例如,你可以将其定义为

 class LargeObj : public std::vector<HugeObj> { // constructor that fills the object with data LargeObj() ; // ... other standard methods ... }; 

只有你可以确定这是否适合你的应用程序。 我的观点是,尽pipeLargeObj “大部分”是一个容器,但是如果这样做对于你的应用程序来说也是可以的。

你做的第一个代码片段特别有用,比如在一个DLL中实现了getObjData(),从另一个DLL中调用它,并且这两个DLL是用不同的语言或不同版本的编译器为同一种语言实现的。 原因是因为当他们被编译在不同的编译器中时,他们经常使用不同的堆。 你必须在同一个堆内分配和释放内存,否则你会损坏内存。 </windows>

但是,如果你不这样做,我通常会简单地返回一个指针(或智能指针)到您的函数分配的内存:

 LargeObj* getObjData() { LargeObj* ret = new LargeObj; ret->fillWithData() ; return ret; } 

…除非我有一个特定的原因不。