Search code examples
c++classtemplatesc++11friend-function

About the return type of an unbound template friend function of a template class


Suppose I have a template class which has a template friend function hoping to implement the function of a value multiplying an array(myArray):

template<typename T, int size>
class myArray{
  T *_array;
public:
  ...
  template<typename Val, typename Array> 
  friend myArray<T,size> operator*(const Val &lhs, const Array &rhs){
    myArray<T,size> mat_t;
    for(int i = 0;i < size; i++)
      mat_t._array[i] = lhs * rhs._array[i];
    return mat_t;
}
  ...
};

It works on VS2013. Then I move the definition of the unbound template friend function outside:

template<typename Val, typename Array>
myArray<T,size> operator*(const Val &lhs, const Array &rhs){
  myArray<T,size> mat_t;
  for(int i = 0;i < size; i++)
    mat_t._array[i] = lhs * rhs._array[i];
  return mat_t;
}

It is incorrect! I suppose the problem is with the return type of the friend function. But I can't not figure it out. So how to define a friend template function like this outside the class declaration?


Solution

  • friend functions declared like the first example are strange beasts. Each instance of myArray<T,Size> creates a distinct friend function that can only be found via argument dependent lookup on myArray<T,Size>.

    A free operator* not declared that way does not work that way.

    You can get your code to work like this:

    template<class Val, class T, int size>
    myArray<T,size> operator*(const Val &lhs, const myArray<T,size> &rhs){
      myArray<T,size> mat_t;
      for(int i = 0;i < size; i++)
        mat_t._array[i] = lhs * rhs._array[i];
      return mat_t;
    }
    

    here all of the template parameters are listed in the template<> list, and they are all deducible from the arguments to *.

    Remember to put your operator* in the same namespace as myArray.

    However, personally, I'd go with:

    friend myArray operator*(const T&lhs, const myArray &rhs){
      myArray mat_t;
      for(int i = 0;i < size; i++)
        mat_t._array[i] = lhs * rhs._array[i];
      return mat_t;
    }
    

    a non-template friend operator*. Again, one of these is "spawned" for each myArray<T,size>, but it itself is not a template. This has certain advantages, such as it behaves nicer with conversion constructors.

    Going a step further, we get:

    friend myArray& operator*=(myArray&mat, const T&scalar){
      for(int i = 0;i < size; i++)
        mat._array[i] *= scalar;
      return mat;
    }
    friend myArray operator*(const T&scalar, myArray mat){
      mat *= scalar;
      return mat;
    }
    friend myArray operator*(myArray mat, const T&scalar){
      mat *= scalar;
      return mat;
    }
    

    where we first create a *=, then write * in terms of it. Notice that * takes myArray by-value (because I need to return a copy anyhow, might as well have it happen outside the method), and I support both mat*scalar and scalar*mat and mat*=scalar.

    Also note that matrices of matrices ... just work.

    The reason why this friend operator is a good idea is illustrated here. Note the code does not compile. Now #define ADL to move the operator* into the class as friends and it compiles. Koenig operators let operator* operate on a template class without being a template, and argument matching rules for templates are hard to get right without being overly narrow, overly broad, or using nasty SFINAE.

    Next step, operator*= can be improved via: (C++14 used for brevity)

    template<class Array
      class=std::enable_if_t<std::is_same<std::decay_t<Array>,myArray>>
    >
    friend Array operator*=(Array&&mat, const T&scalar){
      for(int i = 0;i < size; i++)
        mat._array[i] *= scalar;
      return std::forward<Array>(mat);
    }
    

    which is more complex, but does fun things with perfect forwarding. A temporary myArray gets moved into the return value (which allows for reference lifetime extension to work), while non-temporary myArray return a reference.

    This provides a reason why even *= should be a friend. It allows both r and lvalues to be implemented in the same method.

    On the other hand, possibly you don't want *= to work with rvalues. In that case, use a regular method with a & reference-category qualifier to eliminate that choice.