Search code examples
c++templatesoperator-overloadingcrtpgotw

How can I implement "op" in terms of "op=" in a CRTP base class?


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)?


Solution

  • 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