【问题标题】:Copying big objects, c++复制大对象,C++
【发布时间】:2012-09-18 00:09:27
【问题描述】:

我确信这已得到解答,但我不是程序员,无法找到/理解适当的答案。 假设我有一个庞大的类big,它想重载一个二元运算符,比如operator +

  1. 有没有一种理智的方法可以让big X=Y+Z 直接将总和构建到X 中,而不是 创建临时对象,将其复制到X,然后销毁临时对象?
  2. 我唯一想到的是将big包装到另一个类small中,该类将包含指向big的指针,并添加int use;对big的引用数量,以便big对象当use==0 时被销毁。并添加另一个赋值运算符,例如 <= 用于实际复制。我试图实现它(下)。它似乎工作,但 我没有经验,我很难预见,会出什么问题。 不应该有更简单的解决方案吗?

代码:

#include <iostream>

// print and execute cmd
#define Do(cmd) cout << "\n\n\t"<< ++line << ".\t" << #cmd << ";\n" << endl; cmd;

// print small object: name(small.id[big.id,u=big.use,x=big.x])
#define Show(avar) cout << #avar << "(" << (avar).id << "[" << ((avar).data==NULL?0:(avar).data->id) << ",u=" << ((avar).data==NULL?0:(avar).data->use) << ",x=" << ((avar).data==NULL?0:(avar).data->x) << "])" 

using namespace std;

class big{
public:
  static int N;   // biggest id in use
  int id;         // unique id for each object
  int use;        // nuber of references to this object
  int x;          // data
  big() __attribute__((noinline))
  {
    id=++N;
    use=1;
    x=0;
    cout << "big.constructor.def: [" << id << ",u=" << use << ",x="<<x<<"]" << endl;
  }
  big(const int& y) __attribute__((noinline))
  {
    id=++N;
    x=y;
    use=1;
    cout << "big.constructor.int: [" << id << ",u=" << use << ",x="<<x<<"]" << endl;
  }
  big(const big& b) __attribute__((noinline))
  {
    id=++N;
    use=1;
    x=b.x;
    cout << "big.constructor.copy: [" << id << ",u=" << use << ",x="<<x<<"]" << endl;
  }
  ~big() __attribute__((noinline))
  {
    if(use>0) throw 99; // destroing referenced data!
    cout << "big.destructor: [" << id << ",u=" << use << ",x="<<x<<"]" << endl;
  }
  friend class small;
};

class small{
public:
  static int N;      // biggest id in use
  int id;            // unique id
  big * data;        // reference to the actual data
  small() __attribute__((noinline))
  {
    id=++N;        
    data=NULL;       // contains no data
    cout << "small.constructor.def: ";
    Show(*this)<< endl;
  }
  small(const int& y) __attribute__((noinline))
  {
    id=++N;
    data=new big (y);  // relies on the big constructor
    cout << "small.constructor.int: ";
    Show(*this)<<endl;
  }
  small(const small& y) __attribute__((noinline))
  {
    id=++N;
    data=y.data;      // new object refers to the same data!!
    if(data!=NULL) 
      ++(data->use);  // new reference added;
    cout << "small.constructor.copy: "; 
    Show(y) << "-->";
    Show(*this) << endl;
  }
  ~small(){
    cout << "small.destructor: ";
    Show(*this)<< endl;
    if(data!=NULL){       // is there data?
      --(data->use);      // one reference is destroyed
      if(data->use == 0)  // no references left, kill the data
    delete data;
    }
  }
  const small& operator= (const small& b) __attribute__((noinline))
  {
    cout << "equal: ";
    Show(*this) << " = ";
    Show(b)<<endl;
    if(data != NULL){     // is there data in the target?
      --(data->use);      // one reference is destroyed
      if(data->use == 0)  // no references left, 
    delete data;      // kill the data
    }
    data=b.data;          // target referenses the same data as the source!
    if(data!=NULL) 
      ++(data->use);      // new references added
    cout << "Done equal: "<<endl;    
    return *this;
  }
  // <= will be used for actual copying the data
  const small& operator<= (const small& b) __attribute__((noinline))
  {
    cout << "Copy: ";
    Show(*this) << " <= ";
    Show(b)<<endl;
    if(data != NULL){     // is there data in the target?
      --(data->use);      // one reference is destroyed
      if(data->use == 0)  // no references left, 
    delete data;      // kill the data
    }
    if(b.data==NULL)     // source has no data
      data=NULL;
    else
      data = new big(*(b.data)); // make new copy of the data 
                                 // via big's copy constructor
    cout << "Done copy: "<<endl;    
    return *this;
  }
  small operator+ (const small& b) __attribute__((noinline))
  {
    cout << "Plus: "; 
    Show(*this) << " + ";
    Show(b)<< endl;
    if(this->data == NULL | b.data == NULL) throw 99; // missing data for +
    small ret(data->x);
    ret.data->x += b.data->x;
    cout << "Return: "; Show(ret)<<endl;
    return ret;
  }
};


int big::N=0;
int small::N=0;

main(){
  int line=0;

  Do(small X(5); small Y(6); small Z(7); small W(X));
  Show(X) << endl;
  Show(Y) << endl;
  Show(Z) << endl;
  Show(W) << endl;

  Do(X=Y; Z<=Y);
  Show(X)<<endl;  
  Show(Y)<<endl;  // X and Y refer to the same data
  Show(Z)<<endl;  // Z has a copy of data in Y

  Do(X=Z; Y=Z);
  Show(X)<<endl;
  Show(Y)<<endl;
  Show(Z)<<endl;  // data previosly in X,Y destroyed

  Do(small* U=new small (17); small* T=new small (*U));
  Show(*U) << endl;
  Show(*T) << endl; // U and T refer to the same big

  Do(delete U);
  Show(*T) << endl; // big stays since there is another reference to it

  Do(delete T);     // big destroyed

  Do(X=(Y+Z)+W);
  Show(X)<<endl;
  Show(Y)<<endl;
  Show(Z)<<endl;  // no extra copying of data occures

  cout << "\n\tEND\n" << endl;
}

输出:

1.  small X(5); small Y(6); small Z(7); small W(X);

big.constructor.int: [1,u=1,x=5]
small.constructor.int: *this(1[1,u=1,x=5])
big.constructor.int: [2,u=1,x=6]
small.constructor.int: *this(2[2,u=1,x=6])
big.constructor.int: [3,u=1,x=7]
small.constructor.int: *this(3[3,u=1,x=7])
small.constructor.copy: y(1[1,u=2,x=5])-->*this(4[1,u=2,x=5])
X(1[1,u=2,x=5])
Y(2[2,u=1,x=6])
Z(3[3,u=1,x=7])
W(4[1,u=2,x=5])


    2.  X=Y; Z<=Y;

equal: *this(1[1,u=2,x=5]) = b(2[2,u=1,x=6])
Done equal: 
Copy: *this(3[3,u=1,x=7]) <= b(2[2,u=2,x=6])
big.destructor: [3,u=0,x=7]
big.constructor.copy: [4,u=1,x=6]
Done copy: 
X(1[2,u=2,x=6])
Y(2[2,u=2,x=6])
Z(3[4,u=1,x=6])


    3.  X=Z; Y=Z;

equal: *this(1[2,u=2,x=6]) = b(3[4,u=1,x=6])
Done equal: 
equal: *this(2[2,u=1,x=6]) = b(3[4,u=2,x=6])
big.destructor: [2,u=0,x=6]
Done equal: 
X(1[4,u=3,x=6])
Y(2[4,u=3,x=6])
Z(3[4,u=3,x=6])


    4.  small* U=new small (17); small* T=new small (*U);

big.constructor.int: [5,u=1,x=17]
small.constructor.int: *this(5[5,u=1,x=17])
small.constructor.copy: y(5[5,u=2,x=17])-->*this(6[5,u=2,x=17])
*U(5[5,u=2,x=17])
*T(6[5,u=2,x=17])


    5.  delete U;

small.destructor: *this(5[5,u=2,x=17])
*T(6[5,u=1,x=17])


    6.  delete T;

small.destructor: *this(6[5,u=1,x=17])
big.destructor: [5,u=0,x=17]


    7.  X=(Y+Z)+W;

Plus: *this(2[4,u=3,x=6]) + b(3[4,u=3,x=6])
big.constructor.int: [6,u=1,x=6]
small.constructor.int: *this(7[6,u=1,x=6])
Return: ret(7[6,u=1,x=12])
Plus: *this(7[6,u=1,x=12]) + b(4[1,u=1,x=5])
big.constructor.int: [7,u=1,x=12]
small.constructor.int: *this(8[7,u=1,x=12])
Return: ret(8[7,u=1,x=17])
equal: *this(1[4,u=3,x=6]) = b(8[7,u=1,x=17])
Done equal: 
small.destructor: *this(8[7,u=2,x=17])
small.destructor: *this(7[6,u=1,x=12])
big.destructor: [6,u=0,x=12]
X(1[7,u=1,x=17])
Y(2[4,u=2,x=6])
Z(3[4,u=2,x=6])

    END

small.destructor: *this(4[1,u=1,x=5])
big.destructor: [1,u=0,x=5]
small.destructor: *this(3[4,u=2,x=6])
small.destructor: *this(2[4,u=1,x=6])
big.destructor: [4,u=0,x=6]
small.destructor: *this(1[7,u=1,x=17])
big.destructor: [7,u=0,x=17]

【问题讨论】:

  • 您确定优化器不会直接在X 中构建答案吗?返回值优化?
  • 感谢所有打扰的人,尤其是@juanchopanza,这位堂吉诃德希望拥有这样一个。 g++ -O2 完成这项工作,如果函数返回与所有返回语句相同的本地。据我所知,“移动语义”可能也可以完成这项工作。

标签: c++ assignment-operator copying


【解决方案1】:

有,它叫copy elision。与此案例特别相关的是return value optimization (RVO) named and return value optimization (NRVO)。这意味着允许编译器在某些情况下返回值时省略副本。实现一个简单的加法运算符可能会导致 RVO。

请注意,这是编译器允许执行的优化,但不保证会发生。但是 C++ 具有移动语义,它提供了一种正式的方法,通过该方法可以将一个(通常是临时的)对象的底层数据“移动”到另一个对象,而不会产生不必要的副本。有一篇关于移动语义的文章here

【讨论】:

  • 请注意,在他的特定代码中,他需要 NRVO 而不仅仅是 RVO。许多编译器在调试和发布时实现 RVO,但 NRVO 仅在发布时实现。这是导致调试性能差的一个因素。此外,澄清一下,RVO/NRVO 比通过移动语义“移动”更快
  • 我的编译器 (g++ 4.5.0) 没有这样做,正如我检查的那样。但是,无论如何,谢谢,现在我知道什么是(N)RVO。什么是“移动语义”?有没有办法强制 NRVO?
  • Piotr 的回答有效地内联了(正常的)operator+ 并明确删除了返回值副本,因此您始终可以这样做。
  • @user1672572 没有办法强制它,但如果没有多个 return 语句,你的编译器应该这样做。 GCC 4.5.0 应该没问题。移动语义本身就是一个完整的主题。我会尝试找到一些有用的链接。
  • 即使operator = 重载,编译器是否会执行 NRVO?是否有多个返回语句但返回相同的局部变量? “我会努力寻找一些有用的链接。”请/谢谢。
【解决方案2】:

如果总和是一个复合值,另一种方法是让 big::operator+ 返回一个 sumOfBig 类的实例,该类保留指向 Y 和 Z 的指针或引用。

sumOfBig 可能包含在需要时即时计算总和成分的成员函数。

【讨论】:

  • 这会很困难,因为我需要复杂的代数表达式。在请求 X 值之前,除了 Y 和 Z 可能会发生变化。
  • 要将其推广到任意算术表达式,您正在寻找expression templates
  • 这是一种称为expression templates 的技术的过度简化,它在一些库中用于提供友好的语法而不会产生全部开销。您确定没有一个库可以至少部分满足您的需要吗?
  • 不,我不确定。但这不是基本需求吗?为什么这么复杂?
  • 假设您要在大型矩阵上执行计算。复杂性有助于为对称或三角矩阵选择特定算法,或者仅在元素被访问时计算它。
【解决方案3】:

考虑在这种情况下定义和使用+=

Big a, b, c;

代替:

a = b + c;

做:

a=b;
a+=c;

+=的示例定义:

Big& Big::operator += (const Big& other)
{
   this->a += other.a;
   // ...
   return *this;
}

您可以根据operator += 使您的operator + 在逻辑上相同。

【讨论】:

  • 它没有帮助:a=b 进行复制。
  • 一个新变量,表达式只是分成两个语句(重命名 a=X、b=Y、c=Z 并与您的原始版本进行比较)
  • 没有。整个 b(无论它有多大)都被复制到 a 中,然后执行加法。我的版本需要两个值相加,然后将结果直接放入 a。
  • 然后制作big::sum(const big&amp;,const big&amp;);
【解决方案4】:

首先,打开优化,看看你的编译器实际上做了什么:根据 juanchopanza 的回答,你可以免费获得 (N)RVO。

如果您的编译器无法做到这一点,根据 Piotr 的回答明确删除中间副本可能会改善问题。

如果你真的需要推迟任意复杂算术表达式的计算,你需要表达式模板(正如我在 cmets 中提到的 Nicola 和我):如果你有可变源对象,你可能需要表达式模板写时复制。这些都不是微不足道的。如果你真的需要它们,并且真的找不到一个已经满足你需要的库......好吧,一旦你研究了它们并开始了,我会寻找关于你的表达式模板实现的问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-01-24
    • 1970-01-01
    • 1970-01-01
    • 2015-03-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多