代码之家  ›  专栏  ›  技术社区  ›  swongu

返回函数中的大型对象

c++
  •  37
  • swongu  · 技术社区  · 16 年前

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

    使用对大型对象的引用:

    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 ;
    

    因此直接修改/添加方法 大目标 不是直接访问的解决方案。

    9 回复  |  直到 16 年前
        1
  •  41
  •   David Rodríguez - dribeas    16 年前

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

    关于副本。您发布的代码不使用赋值运算符,而是使用复制构造。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 . 里面的操作 F-() 直接应用于 X ,相当于第一个代码段(按引用传递)。

    如果您没有使用复制构造函数(您是否编写了: xx;x= f-(); )然后它会同时创建 X 川芎嗪 并应用赋值运算符,生成三行输出: x::() / x::() / X::算符= . 所以在某些情况下效率可能会低一点。

        2
  •  11
  •   avakar    16 年前

    使用第二种方法。看起来效率较低,但是C++标准允许拷贝被规避。此优化称为 Named Return Value Optimization 并在大多数当前的编译器中实现。

        3
  •  3
  •   KenE    16 年前

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

    但是,为了清晰起见,您仍然可以使用第二种方法,即使对象中的数据很大,但不必牺牲正确使用智能指针的性能。查看boost中的智能指针类套件。这样,即使外部对象是,内部数据也只分配一次,永远不会复制。

        4
  •  2
  •   anon    16 年前

    避免的方法 任何 复制是为了提供一个特殊的构造函数。如果你 可以重新编写代码,使其看起来像:

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

    如果fillmoughly()返回数据(可能是“大字符串”,那么就有一个采用“大字符串”的构造函数。如果您有这样一个构造函数,那么编译器将非常喜欢构造一个对象,而不进行任何复制来执行返回。当然,这在现实生活中是否有用取决于你的具体问题。

        5
  •  2
  •   rjnilsson    16 年前

    有点惯用的解决方案是:

    std::auto_ptr<LargeObj> getObjData()
    {
      std::auto_ptr<LargeObj> a(new LargeObj);
      a->fillWithData();
      return a;
    }
    
    int main()
    {
      std::auto_ptr<LargeObj> a(getObjData());
    }
    
        6
  •  0
  •   Dima    16 年前

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

        7
  •  0
  •   Simon Broadhead    16 年前

    根据对象的实际大小和操作的频率,不要过于拘泥于效率,因为这两种方式都不会产生明显的效果。只有在确定有必要时,才应以牺牲干净、可读的代码为代价进行优化。

        8
  •  0
  •   Dan Breslau    16 年前

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

    但我想指出,如果 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() 在构造函数中,可以将标志作为参数传递给构造函数。

    更新 (来自您的编辑和注释):从语义上讲,如果值得为创建typedef 大目标 --也就是说,给它命名,而不是简单地引用它 typedef std::vector<HugeObj> --那么你已经在给它提供自己的行为语义的路上了。例如,您可以将其定义为

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

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

        9
  •  -1
  •   John Dibling    16 年前

    当您在一个dll中实现getobjdata(),从另一个dll调用它,并且这两个dll在同一语言中以不同语言或编译器的不同版本实现时,第一个代码段尤其有用。原因是,当它们在不同的编译器中编译时,它们通常使用不同的堆。必须在同一堆中分配和释放内存,否则会损坏内存。 </windows>

    但是,如果您不这样做,我通常只返回一个指针(或智能指针)来存储您的函数分配的内存:

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

    …除非我有特别的理由不这么做。