在函数中返回大对象
比较以下两段代码,第一段使用对大对象的引用,第二段使用大对象作为返回值。 强调“大对象”是指不必要地重复复制对象浪费周期。
使用对大对象的引用:
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(); ),那么它将创buildx和tmp并应用赋值运算符,产生三行输出: 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; }
…除非我有一个特定的原因不。