6

I give the following examples to illustrate my question:

 class BigClass
{
public:
    static int destruct_num;
    friend BigClass operator + (const BigClass &obj1, const BigClass &obj2);
    std::vector<int> abc;

    BigClass()
    {

    }

    ~BigClass()
    {
        destruct_num++;
        std::cout << "BigClass destructor " <<destruct_num<< std::endl;

    }

    BigClass(BigClass&& bc) :abc(std::move(bc.abc))
    {
        std::cout << "move operation is involed" << std::endl;
    }
};

int BigClass::destruct_num = 0;

BigClass operator + (const BigClass &obj1, const BigClass &obj2)
{
    BigClass temp;
    temp.abc = obj1.abc;
    temp.abc.insert(temp.abc.end(), obj2.abc.begin(), obj2.abc.end());

    return temp;

}

int main(void)
{

    BigClass a;
    a.abc = { 1,2,3 };
    BigClass b;
    b.abc = { 5,6,7 };
    BigClass c = a + b;

//  for (auto& v : c.abc)
//      std::cout << v << "  ";

    return 0;


}   

One problem with regard to operator + is that we have to generate a temp BigClass object temporally and then return it. Are there someways to reduce this burden?

feelfree
  • 11,175
  • 20
  • 96
  • 167
  • Possible duplicate: https://stackoverflow.com/questions/4421706/what-are-the-basic-rules-and-idioms-for-operator-overloading – Rakete1111 Jul 12 '17 at 08:13
  • 2
    copy elision? http://en.cppreference.com/w/cpp/language/copy_elision – cppBeginner Jul 12 '17 at 08:14
  • 4
    you can avoid the temporary, but the only way to avoid creating a new instance is not to use `operator=`. `operator+=` does not require you to create a new instance – 463035818_is_not_an_ai Jul 12 '17 at 08:19
  • 1
    How are you calling the `operator+`? If you are initializing a new variable then RVO will eliminate the temporary copy. – Chris Drew Jul 12 '17 at 08:25
  • If you have to return a copy of your object how does creating a temp add to that burden? The temp you create is the one that gets returned after all. Otherwise what would you return? – Galik Jul 12 '17 at 08:27
  • Or to put it another way, the sum of two values is a third value. Where is that third value going to come from if you don't make it? – Galik Jul 12 '17 at 08:32

3 Answers3

7

Generally:

[...] the compilers are permitted, but not required to omit the copy [...]

Anyway your function should be optimised by any modern compiler because copy elision.

Here an example:

The result of the operator+ is in the assembly section:

call    operator+(BigClass const&, BigClass const&)
addl    $12, %esp

As you can see, no copy constructor is invoked in order to copy the result.

Indeed, if we disable the copy elision optimisation in GCC, the result changes:

call    operator+(BigClass const&, BigClass const&)
addl    $12, %esp
subl    $8, %esp
leal    -20(%ebp), %eax
pushl   %eax
leal    -56(%ebp), %eax
pushl   %eax
call    BigClass::BigClass(BigClass&&)
addl    $16, %esp
subl    $12, %esp
leal    -20(%ebp), %eax
pushl   %eax
call    BigClass::~BigClass()
addl    $16, %esp

After the call of operator+ the copy (or move in this case) constructor is called, and after the destructor of the temporary object.

Note that the copy elision is obtained even disabling optimisations (-O0).

The same result is obtained with an older version: GCC 4.4.7.


Since copy elision is not guaranteed for all architectures, you might implement some different solutions.

One possible solution is to avoid the allocation of a temporary variable inside the function, demanding the caller the reservation of that space. In order to do that, you should use a "custom" method and avoid to overload the operator+.

void sum_bigClasses(const BigClass& obj1, const BigClass& obj2, BigClass& output) {
   // output.resize(obj1.size() + obj2.size());
   // std::copy(...);
}

Another solution it could be to implement a non-const operator for sum. An example:

BigClass& operator+=(const BigClass& rhs) {
   // std::copy(rhs.cbegin(), rsh.cend(), std::back_inserter(abc));
   return *this;
}

In this way the class interface allows different strategies:

  • Avoid to allocate 3 different object but only 2, if you don't need to preserve all different states.
  • Allow to allocate 3 different object and avoid temporary construction inside the operator.

Here, the two examples.

The first point:

BigClass o1;
BigClass o2;
// Fill o1 and o2;
o1 += o2;
// only 2 object are alive

The second point:

BigClass o1;
BigClass o2;
// Fill o1 and o2;
BigClass o3 = o1;  // Costructor from o1
o3 += o2;
// three different object

EDIT: Since the function it's a NRVO (the returned expression is not a prvalue) neither the new standard C++17 will guarantee the copy elision.

BiagioF
  • 9,368
  • 2
  • 26
  • 50
1

If you run your code than you see that only 3 destructors are called. That means that the value of tmp object is moved, not copied, because of RVO (Return Value Optimization). The compiler doesn't copy it, because it sees that's not necessary.

banana36
  • 395
  • 3
  • 16
0

The use of temporaries not only wastes memory but also processing time (calculating the sum of N BigClass instances may have quadratic time complexity in N). There is no general solution on avoiding this because it depends on how your objects are used. In this scenario:

BigClass c = a + b;

the compiler is already free (or required, C++17) to use copy elision, as explained by banana36, and the inputs are lvalues, therefore they cannot be changed without potentially causing great surprise.

A different scenario would be:

BigClass f();
BigClass g();

BigClass h = f() + g();

In this case, f() and g() are rvalues and copying both of them is wasteful. The storage of at least one of them could be reused, e.g. one could write an additional operator + overload to optimize the case where the left summand is an rvalue:

BigClass operator +(BigClass &&a, const BigClass &b)
{
    a.abc.insert(a.abc.end(), b.abc.begin(), b.abc.end());
    return std::move(a);
}

This reuses a.abc's storage and avoids copying its contents as long as the capacity is sufficient. A nice side effect is that e.g. summing N objects with 10 elements each will have linear performance because insertion of a constant number of elements at the end of a std::vector has constant amortized cost. But it only works if the right overload of operator + is selected, which e.g. is not the case for std::accumulate. Here's an overview of your main options:

  1. Supply operator +(const BigClass &, const BigClass &) and operator +=, and educate your users on the performance implications of using the former carelessly.
  2. Possibly add overloads for operator +(BigClass &&, const BigClass &) and maybe operator +(const BigClass &, BigClass &&) and operator +(BigClass &&, BigClass &&). Note that if you have both overloads with one rvalue reference, you should absolutely also add the overload with two rvalue references, or else f() + g() will be an ambiguous call. Also note that the overload where the right hand parameter is an rvalue reference is best suited for use with e.g. std::deque and not std::vector because it has lesser time complexity on front insertion, but replacing a vector with a deque is only useful if this use case is common, because deque is otherwise slower than vector.
  3. Provide only efficient operations such as operator += and deal with the frustration of the users (alternatively, give the less efficient operations disparaging names like copyAdd).
Arne Vogel
  • 6,346
  • 2
  • 18
  • 31