0

I'm trying to overload operators. I notice that in doing long expression, the compiler makes temporaries and destroy them at the end of statement (after ;). However, as the expression gets longer or the object that is larger, memory gets occupied for no reason (not that I know of). What I want is to destroy them after used like this:

    Test m3 = m1 - m2*2 + 3*m1;
    /* Temporaries predicted from result, temporaries might 
    have different declarations like 'const Test temp = ...;'
    Test temp_m1 = m1*3;
    Test temp_m2 = m2*2;
    Test temp_m3 = m1 - temp_m2;
    Test m3 = temp_m3 + temp_m1;
    temp_m3.~Test();  //destroyed at the end of statement
    temp_m2.~Test();  //destroyed at the end of statement
    temp_m1.~Test();  //destroyed at the end of statement
           ||
           \/
    Test temp_m1 = m1*3;
    Test temp_m2 = m2*2;
    Test temp_m3 = m1 - temp_m2;
    temp_m2.~Test();  //destroyed after temp_m2 is used
    Test m3 = temp_m3 + temp_m1;
    temp_m3.~Test();  //destroyed after temp_m3 is used
    temp_m1.~Test();  //destroyed after temp_m1 is used
    */

I have tried inputing rvalue to operator overloading functions, but it is not working as temporaries are lvalue. I notice that temporaries are created through move constructor and could take advantage of that, but it is not ideal.

The question is:

  1. Is there any way to differentiate temporaries from normal variables?
  2. If not, is there other way to destroy them after used?
  3. Is there any purpose in destroying them at the end of statement?

Main:

#include <iostream>
#include <iomanip>
class Test {
public:
    Test(const Test& m);
    Test(unsigned int limit = 0, int value = 0);
    Test(Test&& m);
    ~Test();

    Test& operator=(const Test& m);
    Test& operator=(Test&& m);
    Test operator+(const Test& m) const;
    Test operator-(const Test& m) const;
    Test operator*(const int& scalar) const;
    friend std::ostream& operator<<(std::ostream& os, const Test& m);
    void reset();
    unsigned int limit_ = 0;
    std::unique_ptr<int[]> data_;
};
int main(){
    Test m2(2,2);
    Test m1(2,1);
    Test m3 = m1 - m2*2 + 3*m1;
    /* Temporaries predicted from result, temporaries might 
    have different declarations like 'const Test temp = ...;'
    Test temp_m1 = m1*3;
    Test temp_m2 = m2*2;
    Test temp_m3 = m1 - temp_m2;
    Test m3 = temp_m3 + temp_m1;
    temp_m3.~Test();
    temp_m2.~Test();
    temp_m1.~Test();
    */
    std::cout << "RESULT" << std::endl;
    std::cout << m1;
    std::cout << m2;
    std::cout << m3;
}

Result:

Constructor: [ 2     2    ]
Constructor: [ 1     1    ]

Multiplication operation:
Constructor: [ 0     0    ]
[ 1     1    ]
 * 3 = [ 3     3    ]
Move constructor: [ 3     3    ]   //temp_m1
Destructor: Empty


Multiplication operation:
Constructor: [ 0     0    ]
[ 2     2    ]
 * 2 = [ 4     4    ]
Move constructor: [ 4     4    ]   //temp_m2
Destructor: Empty


Subtraction operation:
Constructor: [ 0     0    ]
[ 1     1    ]
 - [ 4     4    ]
 = [ -3    -3   ]
Move constructor: [ -3    -3   ]   //temp_m3
Destructor: Empty


Addition operation:
Constructor: [ 0     0    ]
[ -3    -3   ]
 + [ 3     3    ]
 = [ 0     0    ]
Move constructor: [ 0     0    ]   //m3
Destructor: Empty

Destructor: [ -3    -3   ]   //temp_m3 destroyed at the end of statement

Destructor: [ 4     4    ]   //temp_m2 destroyed at the end of statement

Destructor: [ 3     3    ]   //temp_m1 destroyed at the end of statement

RESULT
[ 1     1    ]
[ 2     2    ]
[ 0     0    ]
Destructor: [ 0     0    ]

Destructor: [ 1     1    ]

Destructor: [ 2     2    ]


Process returned 0 (0x0)   execution time : 0.125 s

Implementation code:

Test::~Test(){
        std::cout << "Destructor: " << *this << std::endl;
        data_.reset();
}
Test::Test(Test&& m){  //Move constructor
    std::cout << "Move constructor: ";
    data_ = std::move(m.data_);
    limit_ = m.limit_;
    m.reset();
    std::cout << *this;
}
Test::Test(const Test& m){  //Copy constructor
    std::cout << "Copy constructor: ";
    data_ = std::make_unique<int[]>(m.limit_);
    limit_ = m.limit_;
    for(unsigned int i=0; i<limit_ ; ++i) data_[i] = m.data_[i];
    std::cout << *this;
}
Test::Test(unsigned int limit, int value){    //Constructor
    std::cout << "Constructor: ";
    data_ = std::make_unique<int[]>(limit);
    limit_ = limit;
    if(limit == 0) return;
    for(unsigned int i=0; i<limit ; ++i) data_[i] = value;
    std::cout << *this;
}
Test& Test::operator=(const Test& m){ //Copy assignment
    std::cout << "Copy assignment: ";
    Test local_m = m;

    limit_ = local_m.limit_;
    data_ = std::move(local_m.data_);
    std::cout << *this << std::endl;
    return *this;
}
Test& Test::operator=(Test&& m){  //Move assignment
    std::cout << "Move assignment: ";
    Test local_m = std::move(m);

    limit_ = local_m.limit_;
    data_ = std::move(local_m.data_);
    std::cout << *this << std::endl;
    return *this;
}
Test Test::operator+(const Test& m) const{ //Addition
    std::cout << std::endl<< "Addition operation: " <<std::endl;
    if( limit_ != m.limit_){
        std::cout << "Addition error: not same dimensions, return Empty" <<std::endl;
        return Test();
    }
    if( limit_ == 0){
        std::cout << "Addition warning: Empty, return Empty" <<std::endl;
        return Test();
    }
    Test result(limit_);
    for ( unsigned int i = 0; i < limit_; ++i ) result.data_[i] = data_[i] + m.data_[i];
    std::cout << *this << " + " << m << " = " << result;
    return result;
}
Test Test::operator-(const Test& m) const{ //Subtraction
    std::cout << std::endl<< "Subtraction operation: " <<std::endl;
    if( limit_ != m.limit_){
        std::cout << "Subtraction error: not same dimensions, return Empty" <<std::endl;
        return Test();
    }
    if( limit_ == 0){
        std::cout << "Subtraction warning: Empty, return Empty" <<std::endl;
        return Test();
    }
    Test result(limit_);
    for ( unsigned int i = 0; i < limit_; ++i ) result.data_[i] = data_[i] - m.data_[i];
    std::cout << *this << " - " << m << " = " << result;
    return result;
}
Test Test::operator*(const int& scalar) const{ //Multiplication
    std::cout << std::endl<< "Multiplication operation: " <<std::endl;
    if( limit_ == 0){
        std::cout << "Multiplication warning: Empty, return Empty" <<std::endl;
        return Test();
    }
    Test result(limit_);
    for ( unsigned int i = 0; i < limit_; ++i ) result.data_[i] = data_[i] * scalar;
    std::cout << *this << " * " << scalar << " = " << result;
    return result;
}
Test operator*(const int& scalar, const Test& m){ //Multiplication
    return m * scalar;
}
std::ostream& operator<<(std::ostream& os, const Test& m){  //Show
    unsigned int limit = m.limit_;
    if (limit == 0) return os << "Empty" << std::endl;
    os << "[";
    for ( unsigned int i = 0; i < limit; ++i ){
            os << " " << std::left << std::setw(5) << m.data_[i];
    }
    os << "]" << std::endl;
    return os ;
}
void Test::reset(){ //Reset
        limit_ = 0;
        data_.reset();
}
  • They're not "hidden variables", they are "temporaries" (or "temporary objects"). They will be destroyed when they are no longer needed, generally at the end of the statement that are in. Don't manually call the destructor. You can make use of `{ }` to make inner variable scopes to clean up your named temporary variables when you no longer need them. – 1201ProgramAlarm Mar 11 '20 at 15:59
  • @1201ProgramAlarm Thank you, I will look into temporaries. I know that they will be destroyed **at the end** of the statement based on analysing the _Result_, but doing so takes memory **in mid operation**, I want them destroyed right after being used to free up memory. I didn't call the destructor, that is just the order I want temporaries to be destroyed. – Phạm Quang Trường Mar 11 '20 at 17:51
  • The standard says they are destroyed at the end, you cannot change this. – M.M Mar 11 '20 at 21:06
  • You could try using the recommended idiom of implementing `+` as a free function `T operator+(T a, T const& b) { return a += b; }` – M.M Mar 11 '20 at 21:07
  • @M.M Could you elaborate on that? I don't know that idiom. – Phạm Quang Trường Mar 12 '20 at 05:38
  • @PhạmQuangTrường https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading – M.M Mar 12 '20 at 07:15

1 Answers1

0

Is there any way to differentiate temporaries from normal variables?

This is what rvalue references are for -- an rvalue ref arg (with &&) can only bind to an unnamed temp, while an lvalue ref arg (with &) can only bind to a named (non-temp) value. So you can overload your operators with versions that take either rvalue or lvalue refs to avoid unnecessary copies.

Now while you can have multiple overloaded operators special casing all the different possible combinations, that is generally not necessary. If you follow some basic guidelines, its actually not too bad. Generally you want to overload binary operators as free functions (not methods) that call assignment operators. If you define:

Test &Test::operator +=(const Test &a) {
    // add 'a' into this
    return *this; }

Test operator+(Test a, const Test &b) { return a += b; }  // free function, not a method

and have a move constructor, then generally the number of copies required will be minimized.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • I tried it with `Test operator-(Test&& m)` and `Test operator+(Test&& m)`, but that didn't eliminate every cases. Do I have to do it like this `Test operator-(Test&& m, const Test& m)`..., 4 variances for each operators. Is there better way? – Phạm Quang Trường Mar 12 '20 at 05:47
  • Do you mean **copies** as in **local variables** of function or **temporaries** in expression? – Phạm Quang Trường Mar 12 '20 at 08:15