在函数中返回大对象

时间:2009-04-15 19:25:49

标签: c++

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

使用对大对象的引用:

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() ;
}

第一段代码不需要复制大对象。

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

我认为第二个片段更容易阅读,但我担心效率会降低。

编辑:通常情况下,我认为案例LargeObj是通用容器类,为了参数,它们包含数千个对象。例如,

typedef std::vector<HugeObj> LargeObj ;

因此直接修改/添加LargeObj方法并不是一种直接可用的解决方案。

9 个答案:

答案 0 :(得分:43)

第二种方法更具惯用性和表现力。很明显,在读取代码时,函数对参数没有先决条件(它没有参数),并且它实际上会在里面创建一个对象。对于随意的读者来说,第一种方法并不是那么清楚。调用意味着对象将被更改(通过引用传递)但是如果传递的对象上有任何先决条件则不太清楚。

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

#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 对象保留堆栈中的空间,然后通过 x 调用构造 tmp 的函数(事实上 tmp x f()中的操作直接应用于 x ,相当于到您的第一个代码段(通过引用传递)。

如果您没有使用复制构造函数(如果您编写: X x; x = f(); )那么它将同时创建 x tmp 并应用赋值运算符,产生三行输出: X :: X() / X :: X() / X: :操作= 的。因此,在案件中效率可能会低一些。

答案 1 :(得分:11)

使用第二种方法。似乎效率较低,但C ++标准允许避免副本。此优化称为Named Return Value Optimization,并在大多数当前编译器中实现。

答案 2 :(得分:3)

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

但是,即使对象中的数据很大而且在没有通过正确使用智能指针的情况下牺牲性能,您仍然可以使用第二种方法。查看boost中的智能指针类套件。这样,即使外部对象是。

,内部数据也只会被分配一次并且永远不会被复制

答案 3 :(得分:2)

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

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

如果fillsomehow()返回数据(也许是一个“大字符串”,那么有一个构造函数,它接受一个“大字符串”。如果你有这样的构造函数,那么编译器将非常喜欢构造一个单独的对象而不是任何完全复制以执行返回。当然,这在现实生活中是否有用,取决于您的特定问题。

答案 4 :(得分:2)

有点惯用的解决方案是:

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

int main()
{
  std::auto_ptr<LargeObj> a(getObjData());
}

答案 5 :(得分:0)

或者,您可以通过让对象获取自己的数据来避免这个问题,即。即通过使getObjData()成为LargeObj的成员函数。根据您的实际情况,这可能是一个很好的方式。

答案 6 :(得分:0)

取决于对象的实际大小以及操作发生的频率,当效率无论如何都没有明显影响时,不要过于陷入效率。只有在确定有必要时,才会以清洁,可读的代码为代价进行优化。

答案 7 :(得分:0)

当您通过复制返回时,可能会浪费一些周期。是否值得担心取决于对象的实际大小,以及调用此代码的频率。

但是我想指出,如果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实例初始化为已知且有用的状态,并且浪费的周期更少。

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

更新(来自您的编辑和评论):从语义上讲,如果为LargeObj创建一个typedef是值得的 - 也就是说,给它一个名字,而不是简单地引用它作为typedef std::vector<HugeObj> - 然后你已经开始给它自己的行为语义了。例如,您可以将其定义为

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

只有您可以确定这是否适合您的应用。我的观点是,即使LargeObj“主要”是一个容器,如果这样做适用于您的应用程序,您仍然可以给它类行为。

答案 8 :(得分:-1)

当您执行诸如在一个DLL中实现getObjData(),从另一个DLL调用它,以及两个DLL以不同语言或相同语言的编译器的不同版本实现时,您的第一个代码段特别有用。原因是因为当它们在不同的编译器中编译时,它们通常使用不同的堆。您必须从同一堆中分配和释放内存,否则您将损坏内存。 </windows>

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

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

......除非我有特殊原因不这样做。