Search code examples
c++operator-overloadinglanguage-lawyerpass-by-referencepass-by-value

Canonical implementation of operator+ involves additional move constructor


Motivated by this question, I compared two different versions of an implementation of a binary operator+ in terms of operator+=. Consider we are inside the definition of class X.

Version 1

friend X operator+(X lhs, const X& rhs)   
{
   lhs += rhs;  
   return lhs; 
}

Version 2

friend X operator+(const X& lhs, const X& rhs) 
{    
   X temp(lhs);
   temp += rhs;
   return temp;
}

friend X operator+(X&& lhs, const X& rhs) 
{    
   lhs += rhs;
   return std::move(lhs);
}

Where, in both cases, operator+= is defined as follows:

X& operator+=(const X& rhs)    
{                             
  ... // whatever to add contents of X
  return *this;   
}

Now, I just run the following code and tracked calls of copy/move constructors:

X a, b, c;
X d = a + b + c;

With the first "canonical" version, there were 1 copy + 2 move constructor calls, while with the second version there were just 1 copy + 1 move constructor calls (tested with GCC 10 and -O3).

Question: What hinders the elision of that additional move constructor call in the first case?

Live demo: https://godbolt.org/z/GWEnHJ


Additional observation: In the live demo, where the class has some contents (integer member variable), the move constructor calls are not/are inlined with the first/second version, respectively. Also, with the second version, the final result 6 is calculated at compile time and hard-coded into the assembly (when passed to operator<<), while with the first version, it is read from memory. Generally, the second version seems to be (relatively) much more efficient. But this was likely caused by those cout messages involved. Without them, the assembly output was exactly the same.


Solution

  • What hinders the elision of that additional move constructor call in the first case?

    The defect report DR1148 that was accepted and included in C++11.

    In short, it says (emphasis mine):

    It is unclear whether copy elision is permitted when returning a parameter of class type. If not, it should still be possible to move, rather than copy, the return value.

    Suggested resolution: Amend paragraph 34 to explicitly exclude function parameters from copy elision. Amend paragraph 35 to include function parameters as eligible for move-construction.

    The result can be found in [class.copy.elision]/1.1 (emphasis mine)

    in a return statement in a function with a class return type, when the expression is the name of a non-volatile object with automatic storage duration (other than a function parameter or a variable introduced by the exception-declaration of a handler ([except.handle])) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the object directly into the function call's return object