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 +
.
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?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]
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.