2

Herb Sutter's Guru of the Week #4, "Class Mechanics", teaches that the "a op b" form of an overloaded operator should be implemented in terms of the "a op= b" form (see point #4 in the solutions).

As an example, he shows how do to this for the + operator:

T& T::operator+=( const T& other ) {
    //...
    return *this;
}

T operator+( T a, const T& b ) {
    a += b;
    return a;
}

He points out that first parameter in operator+ is intentionally being passed by value, so that it can be moved if the caller passes a temporary.

Notice that this requires that the operator+ be a non-member function.

My question is, how can I apply this technique to an overloaded operator in a CRTP base class?

So say this is my CRTP base class with its operator+=:

template <typename Derived>
struct Base
{
    //...

    Derived operator+=(const Derived& other)
    {
        //...
        return static_cast<Derived&>(*this);
    }
};

I can see how to implement operator+ in terms of operator+= as a member function if I dispense with the "pass the first argument by value" optimization:

template <typename Derived>
struct Base
{
    //...

    Derived operator+(const Derived& other) const
    {
        Derived result(static_cast<const Derived&>(*this);
        result += other;
        return result;
    }
};

but is there a way to do this while using that optimization (and therefore making the operator+ a nonmember)?

HighCommander4
  • 50,428
  • 24
  • 122
  • 194
  • Why is Base not a Base : Derived (where base is misleading anyways) ? –  Apr 30 '14 at 00:17
  • Hmmm... shouldn't `T operator+(T, const T&)` be declared as `T operator+(T&&, const T&)` and forwarded to be moved? And to be sure everything works, and won't make unnecessary copies? (I guess that would happen if someone passed a reference there, or would compiler produce an error?). I honestly hardly find it an optimization. Can someone enlighten me what overhead happens, when you call a member version of `operator+` on a temporary? – luk32 Apr 30 '14 at 00:19
  • 1
    @DieterLücking: Because that's not how the CRTP works? A concrete derived class `D` will derive from `Base`. – HighCommander4 Apr 30 '14 at 00:25
  • @luk32: A member version of `operator+` has to copy `*this` before calling `operator+=`, regardless whether `*this` is a temporary or not. A non-member version of `operator+` taking its first argument by value avoids this copy in the event the first argument is temporary. – HighCommander4 Apr 30 '14 at 00:27
  • 2
    `friend`: http://coliru.stacked-crooked.com/a/a4c97593adf5a080 – Mooing Duck Apr 30 '14 at 00:32
  • 1
    @HighCommander4: `T operator+(T&&, const T&)` won't allow an lvalue to bind to the left side. `T a,b; a+=b;` would fail to compile. He picked that signature for a reason. Values for mutable, `const&` for immutable. `&&` is really only for move constructors and a few other special cases. – Mooing Duck Apr 30 '14 at 00:34
  • @MooingDuck: Whoops, you're right! The name of the class being `T` made be confuse `T&&` with a universal reference :) – HighCommander4 Apr 30 '14 at 00:38
  • @MooingDuck Makes me only wonder. Y U NO ANSWER? =) It looks really nice. You expect it to fail somehow? I thought `&&` would make a copy in case of an `lvalue`. OP no need to repeat =). – luk32 Apr 30 '14 at 00:42
  • @MooingDuck: Nice! Out of curiosity, is there a way to do it without declaring `operator+` as a friend function in the class (since it doesn't actually need access to anything private or protected in the class)? – HighCommander4 Apr 30 '14 at 00:47
  • @HighCommander4: There is, I've added it to my answer. It's weird though, and makes confusing error messages even worse :( – Mooing Duck Apr 30 '14 at 01:01

1 Answers1

4

The normal way to implement Herb's advice is as follows:

struct A {
      A& operator+=(cosnt A& rhs)
      {
          ...
          return *this;
      }
      friend A operator+(A lhs, cosnt A& rhs)
      {
          return lhs += rhs;
      }
};

Extending this to CRTP:

template <typename Derived>
struct Base
{
    Derived& operator+=(const Derived& other)
    {
        //....
        return *self();
    }
    friend Derived operator+(Derived left, const Derived& other)
    {
        return left += other;
    }
private:
    Derived* self() {return static_cast<Derived*>(this);}
};

If you try to avoid the use of friend here, you realize it's almost this:

 template<class T>
 T operator+(T left, const T& right) 
 {return left += right;}

But is only valid for things derived from Base<T>, which is tricky and ugly to do.

template<class T, class valid=typename std::enable_if<std::is_base_of<Base<T>,T>::value,T>::type>
T operator+(T left, const T& right) 
{return left+=right;}

Additionally, if it's a friend internal to the class, then it's not technically in the global namespace. So if someone writes an invalid a+b where neither is a Base, then your overload won't contribute to the 1000 line error message. The free type-trait version does.


As for why that signature: Values for mutable, const& for immutable. && is really only for move constructors and a few other special cases.
 T operator+(T&&, T) //left side can't bind to lvalues, unnecessary copy of right hand side ALWAYS
 T operator+(T&&, T&&) //neither left nor right can bind to lvalues
 T operator+(T&&, const T&) //left side can't bind to lvalues
 T operator+(const T&, T) //unnecessary copies of left sometimes and right ALWAYS
 T operator+(const T&, T&&) //unnecessary copy of left sometimes and right cant bind to rvalues
 T operator+(const T&, const T&) //unnecessary copy of left sometimes
 T operator+(T, T) //unnecessary copy of right hand side ALWAYS
 T operator+(T, T&&) //right side cant bind to lvalues
 T operator+(T, const T&) //good
 //when implemented as a member, it acts as if the lhs is of type `T`.

If moves are much faster than copies, and you're dealing with a commutative operator, you may be justified in overloading these four. However, it only applies to commutative operators (where A?B==B?A, so + and *, but not -, /, or %). For non-commutative operators, there's no reason to not use the single overload above.

T operator+(T&& lhs , const T& rhs) {return lhs+=rhs;}
T operator+(T&& lhs , T&& rhs) {return lhs+=rhs;} //no purpose except resolving ambiguity
T operator+(const T& lhs , const T& rhs) {return T(lhs)+=rhs;} //no purpose except resolving ambiguity
T operator+(const T& lhs, T&& rhs) {return rhs+=lhs;} //THIS ONE GIVES THE PERFORMANCE BOOST
Mooing Duck
  • 64,318
  • 19
  • 100
  • 158
  • Thanks! One small correction: "when implemented as a member, it acts as if the lhs is of type `T`" --> shouldn't that be "`T&` or `const T&` depending on whether the member function is `const`"? – HighCommander4 Apr 30 '14 at 02:42
  • @HighCommander4: well, technically it acts like a universal reference, which in practice acts like the `T` overloads, but ever so slightly better. When you have `T&` or `T&&`, (and they're not universal references), those limit what you can pass in, wheras `T` and universal references do not limit. – Mooing Duck Apr 30 '14 at 02:44
  • Right, but `T` creates a new object for the parameter (whether by copy or move), while in a member function you have to copy `*this` yourself. – HighCommander4 Apr 30 '14 at 03:16
  • I know it is a late comment, but I took my time to read the related post and its comments. In fact there was a discussion about the signature, and my question/problem wasn't that stupid after all. The "good" signature do not support everything. To support all cases properly you would need 4 overloads, [relevant comment](http://herbsutter.com/2013/05/20/gotw-4-class-mechanics/#comment-10539), `T operator+(T&&, const T&)` being in there, so maybe I wasn't so stupid. It was decided to force/implement most popular convention. – luk32 May 02 '14 at 01:38
  • @luk32: Good point, thanks for pointing that out! (Did anyone suggest your comment was stupid?) – HighCommander4 May 02 '14 at 06:53
  • @HighCommander4 No. Of course not. I just got such a feeling. It was the effect of someone really knowledgeable saying it was wrong, and the used one was good. Well I was mistaken, but I thought it was black and white, while apparently it is not. – luk32 May 02 '14 at 12:38
  • @luk32: If moving is expensive, or unusual type-specific optimizations are possible, sometimes you do want overloads, yes. However, _if_ the body is implemented as `return lhs+=rhs;` (and moves are cheap, yada-yada) then `T operator+(T, const T&)` itself binds to all of the same things as those four do, with zero overhead. There are exceptions, but that comment doesn't seem to be mentioning them. (There are also rare cases where you want to take `T&` which the comment doesn't mention at all) Notice that Herb himself recommends only the overload I suggested. – Mooing Duck May 02 '14 at 16:22
  • @luk32: Now that I reread more of the comments, I see the issue. If a move is much faster than a copy, then yes, providing those four overloads may be justified, I've added a note to the answer. – Mooing Duck May 02 '14 at 16:44