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

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

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

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

然后我打电话给这个函数:

 Object object = f(); 

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

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

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

编辑

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

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

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

 Object f() { return Object(); } 

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

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

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

当满足某些条件时,允许实现省略类对象的复制结构,即使该对象的复制构造函数和/或析构函数具有副作用。 在这种情况下,实现将省略的复制操作的源和目标视为仅仅两种不同的引用同一对象的方式,并且该对象的销毁发生在两个对象在没有优化的情况下被销毁的时间的后期。 。 在下列情况下允许这种复制操作的优点(可以组合使用以消除多个副本):

  • 在具有类terturn类型的函数的return语句中,当表达式是具有与函数返回类型相同的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 f() { return std::auto_ptr(new Derived); } 

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

这称为返回值优化 (或RVO)

编译器将对其进行优化。

除了在某些情况下, 例如 :

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

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

编译器是否可以应用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完全是可选的并且不容易检查。

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

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

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

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

但是,返回一个智能指针(例如shared_ptr可以减轻客户端不必记住显式释放内存,这比使用它更合理。

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

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

使用:

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

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

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

这条路:

  • 你肯定避免复制。

  • 你避免使用堆。

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

另一个替代方法是使函数成为Object的成员,但这可能不合适,具体取决于f()的契约。