从函数创建并返回一个大对象

时间:2011-01-26 19:36:17

标签: c++ c memory

想象一下这种情况,我有这样的功能:

Object f()
{
    Object obj;
    return obj;
}

sizeof(Object)是一个很大的值。

然后我调用了这个函数:

Object object = f();  

我是否正确理解第一个Object将在堆栈上创建(在函数中)然后将被复制到对象变量?

如果是这样,是否合理地在堆上的函数中创建一个对象并返回指向它而不是副本的指针?

但我的意思是必须在f()函数中创建对象 - 不是通过指针或对此函数的引用传递并初始化。

修改

我不是说f是一个非常简单的功能。根据某些上下文,它可能有一个非常复杂的对象初始化例程。编译器是否仍会优化它?

7 个答案:

答案 0 :(得分:18)

对于这种特定情况,您可以利用现在的编译器足够智能来优化它的事实。优化称为名为return value optimization(NRVO),因此可以返回类似的“大”对象。编译器可以看到这样的机会(特别是在代码片段这么简单的事情中)并生成二进制文件以便不会创建副本。

您还可以返回未命名的临时工具:

Object f()
{
    return Object();
}

这将在几乎所有现代C ++编译器上调用(未命名)返回值优化(RVO)。实际上,即使关闭所有优化,Visual C ++也会实现此特定优化。

C ++标准特别允许这些优化:

  

ISO 14882:2003 C ++标准,§12.8段。 15:   复制类对象

     

当符合某些标准时,a   允许实现省略   复制类对象的构造,   即使是复制构造函数和/或   对象的析构函数有侧面   效果。在这种情况下,   实现对待源和   省略的复制操作的目标   仅仅是两种不同的方式   指的是同一个对象,而且   发生该对象的破坏   两个时代的晚些时候   物体本来就被摧毁了   没有优化。这个精灵   复制操作是允许的   以下情况(可能是   结合消除多重   份):

     
      
  • 在具有类terturn类型的函数的return语句中,   当表达式是a的名称时   用非易失性自动对象   同样的cv-unqualified类型   函数返回类型,副本   操作可以省略   构造自动对象   直接进入函数的返回   值
  •   
  • 当一个尚未绑定到引用的临时类对象时   将被复制到一个类对象   相同的cv-unqualitied类型,副本   操作可以省略   构造临时对象   直接进入了目标   省略了副本。
  •   

通常,编译器将始终尝试实现NRVO和/或RVO,尽管在某些情况下可能无法执行此操作,例如多个返回路径。然而,这是一个非常有用的优化,你不应该害怕使用它。

如果有疑问,您可以随时通过插入“调试语句”来测试编译器,并亲眼看看:

class Foo
{
public:
    Foo()                      { ::printf("default constructor\n"); }
    // "Rule of 3" for copyable objects
    ~Foo()                     { ::printf("destructor\n");          }
    Foo(const Foo&)            { ::printf("copy constructor\n");    }
    Foo& operator=(const Foo&) { ::printf("copy assignment\n");     } 
};

Foo getFoo()
{
    return Foo();
}

int main()
{
    Foo f = getFoo();
}

如果返回的对象不是可复制的,或者(N)RVO失败(可能不太可能发生),那么您可以尝试返回代理对象:

struct ObjectProxy
{
private:
    ObjectProxy() {}
    friend class Object;    // Allow Object class to grab the resource.
    friend ObjectProxy f(); // Only f() can create instances of this class.
};

class Object
{
public:
    Object() { ::printf("default constructor\n"); }
    ~Object() { ::printf("destructor\n"); }
    // copy functions undefined to prevent copies
    Object(const Object&);
    Object& operator=(const Object&);
    // but we can accept a proxy
    Object(const ObjectProxy&)
    {
        ::printf("proxy constructor\n");
        // Grab resource from the ObjectProxy.
    }
};

ObjectProxy f()
{
    // Acquire large/complex resource like files
    // and store a reference to it in ObjectProxy.
    return ObjectProxy();
}

int main()
{
     Object o = f();
}

当然,这并不是很明显,因此需要适当的文档(至少是对它的评论)。

您还可以将某种智能指针(如std::auto_ptrboost::shared_ptr或类似的东西)返回到免费商店中分配的对象。如果需要返回派生类型的实例,则需要这样做:

class Base {};
class Derived : public Base {};

// or boost::shared_ptr or any other smart pointer
std::auto_ptr<Base> f()
{
    return std::auto_ptr<Base>(new Derived);
}

答案 1 :(得分:2)

理论上你所描述的是应该发生的事情。无论如何编译器通常能够以某种方式对其进行优化,使用调用者Objectf将直接写入调用者的对象并返回null。

这称为Return Value Optimization(或RVO)

答案 2 :(得分:2)

  

我首先理解正确吗?   将在堆栈上创建对象(在   该功能)然后将被复制   对象变量?

是在堆栈上创建了obj,但是当您返回一个名为返回值优化的进程或RVO时,可以防止不必要的复制。

  

如果是这样,是否合理地创建了一个   对象在堆上的函数和   返回一个指针而不是一个   复制?

是的,只要您清楚地记录客户端负责清理内存,就可以在堆上创建一个对象并返回指向它的指针。

但是,返回一个智能指针(例如shared_ptr<Object>)可以减轻客户端必须记住明确释放内存的效果,这是合理的。

答案 3 :(得分:2)

编译器将对其进行优化。

除了在某些情况下,such as

std::string f(bool cond = false)
{
  std::string first("first");
  std::string second("second");
  // the function may return one of two named objects
  // depending on its argument. RVO might not be applied
  if(cond)
    return first;
  else
    return second;
}

当然可以有一些旧的编译器,可以调用复制构造函数。但你不应该担心现代编译器。

答案 4 :(得分:2)

编译器是否可以应用RVO取决于所涉及的实际代码。一般准则是尽可能晚地创建返回值。例如:

std::string no_rvo(bool b) {
  std::string t = "true", f = "fals";

  f += t[3];  // Imagine a "sufficiently smart compiler" couldn't delay initialization
  // for some reason, such not noticing only one object is required depending on some
  // condition.

  //return (b ? t : f);  // or more verbosely:
  if (b) {
    return t;
  }
  return f;
}

std::string probably_rvo(bool b) {
  // Delay creation until the last possible moment; RVO still applies even though
  // this is superficially similar to no_rvo.
  if (b) {
    return "true";
  }
  return "false";
}

使用C ++ 0x,编译器可以自由地做出更多假设,主要是通过使用移动语义。这些工作如何成为一种“蠕虫”,但正在设计移动语义,以便它们可以应用于上面的确切代码。这在no_rvo情况下最有帮助,但它在两种情况下都提供了保证的语义,因为移动操作(如果可能)优先于复制操作,而RVO完全是可选的,不容易检查。

答案 5 :(得分:1)

如果函数f是工厂方法,最好返回指针或初始化的智能指针对象,如auto_ptr。

auto_ptr<Object> f()
{
     return auto_ptr<Object>(new Object);
}

使用:

{    
    auto_ptr<Object> myObjPtr = f();
    //use myObjPtr . . . 
} // the new Object is deleted when myObjPtr goes out of scope

答案 6 :(得分:1)

我不知道为什么没有人指出明显的解决方案。只需通过引用传递输出对象:

void f(Object& result) {
  result.do_something();
  result.fill_with_values(/* */);
};

这样:

  • 你肯定会避免复制。

  • 你避免使用堆。

  • 你要避免让调用代码负责释放动态分配的对象(尽管shared_ptr或unique_ptr也可以这样做)。

另一种方法是使该函数成为Object的成员,但这可能不合适,具体取决于f()的合同。