Domanda

I am sure this has been answered, but I am no programmer and couldn't find/understand the appropriate answer. Suppose I have a massive class big for which would like to overload a binary operator, say operator +.

  1. Is there a sane way to make big X=Y+Z to build the sum directly into X, instead of creating temporary object, copying it to X and then destroying the temporary?
  2. The only thing that come into my mind is to wrap big into another class small that will contain the pointer to big and add int use; number of references to to big, so that big object get destroyed when use==0. And add another assignment operator, say <= for actual copying. I tried to implement it (below). It seems to work, but I have no experience and it is hard for me to foresee, what can go wrong. Shouldn't be there a simpler solution?

Code:

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

Output:

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]
È stato utile?

Soluzione

There is, it is called copy elision. Of particular relevance to this case are return value optimization (RVO) named and return value optimization (NRVO). It means the compiler is allowed to elide copies when returning values in certain situations. Implementing a naive addition operator is likely to result in RVO.

Note that this is an optimization that the compiler is allowed to do, but it isn't guaranteed to take place. But C++ has move semantics, which provide a formal means by which the underlying data of one (usually temporary) object can be "moved" to another object, without incurring in unnecessary copies. There's an article on move semantics here.

Altri suggerimenti

If the sum is a composite value an alternative approach would be to have big::operator+ return an instance of a sumOfBig class which keeps pointers or references to Y and Z.

sumOfBig might contain member functions that computed the sum constituents on the fly, when needed.

Consider to define and use += in such case:

Big a, b, c;

Instead of:

a = b + c;

Do:

a=b;
a+=c;

Example definition of +=:

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

You can make your operator + based on operator += to make them do logically the same.

First, turn on optimization and see what your compiler actually does: you may get (N)RVO for free as per juanchopanza's answer.

If your compiler can't do that, explicitly eliding the intermediate copy as per Piotr's answer might improve matters.

If you really need to defer evaluation of arbitrarily complex arithmetic expressions, you need expression templates (as Nicola and I mentioned in comments): if you have mutable source objects, you may need expression templates and copy-on-write. Neither of these are trivial. If you really need them, and really can't find a library that already does what you need ... well, I'll look out for questions about your expression template implementation, once you've researched them and made a start.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top