关于模板类的未绑定模板友元函数的返回类型

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

本文关键字:友元 函数 返回类型 绑定 于模板      更新时间:2023-10-16

假设我有一个模板类,它有一个模板友函数,希望实现一个值乘以数组(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;
}
  ...
};

它在VS2013上工作。然后将未绑定模板友元函数的定义移到外面:

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;
}

是不正确的!我想问题出在朋友函数的返回类型上。但我不可能不明白。那么如何在类声明之外定义像这样的友元模板函数呢?

像第一个例子那样声明的friend函数是奇怪的野兽。myArray<T,Size>的每个实例创建一个不同的 friend函数,该函数只能通过参数依赖查找在myArray<T,Size>上找到。

没有这样声明的自由operator*不能这样工作。

你可以让你的代码这样工作:

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;
}

这里所有的模板形参都列在template<>列表中,它们都可以从*的参数中推导出来。

请记住将operator*myArray放在同一个命名空间中。

然而,我个人认为:

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;
}

一个非template的朋友operator*。同样,其中一个是为每个myArray<T,size>"衍生"的,但它本身不是template。这有一定的优点,比如它在转换构造函数中表现得更好。

更进一步,我们得到:

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;
}

,我们首先创建一个*=,然后用它写*。请注意,*按值接受myArray(因为无论如何我都需要返回一个副本,不妨让它发生在方法之外),并且我同时支持mat*scalarscalar*matmat*=scalar

还要注意矩阵的矩阵…只是工作。

为什么这个friend operator是一个好主意在这里说明。注意,代码不能编译。现在#define ADLoperator*作为friends移动到类中并进行编译。Koenig操作符允许operator*在没有模板的情况下对template类进行操作,而模板的参数匹配规则如果不太窄、太宽或使用令人不快的SFINAE,就很难得到正确的匹配规则。

下一步,operator*=可以通过以下方式改进:(c++ 14为简洁使用)

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);
}

更复杂,但通过完美的转发做了有趣的事情。临时myArraymove d作为返回值(允许引用生命周期扩展工作),而非临时myArray返回引用。

这提供了为什么*=也应该是friend的原因。它允许在同一个方法中实现r值和左值。

另一方面,可能您不希望*=处理右值。在这种情况下,使用带有&引用类别限定符的常规方法来消除这种选择。

TsizemyArray的模板参数。如果你把包含这些模板参数的函数的定义放在myArray的定义之外,你必须再次声明,它们是模板参数,例如

template <typename T, int size, typename Val, typename Array>

然而,即使你编译了它,这对你也没有多大帮助,因为你不能正确地调用操作符,因为编译器不能推断出模板参数Tsize。调用必须是一些尴尬的东西,比如

auto a = operator*<double, 4>(22, someArray);

你可能想让一个函数只返回与它的第二个参数相同的类型——其他任何东西都没有多大意义。为了避免在模板友元声明中遇到的困难(很难正确处理)以及与友元之间的紧密耦合,可以将操作符传递给公共乘法函数,或者更好的做法是,operator*=

template<typename T, int size>
class myArray {
  T *_array;
public:
  myArray() : _array(new T[size]{22}) {}
  //...
  template<typename Val> 
  myArray& operator*=(const Val &val) {
    for (int i = 0; i < size; ++i) 
      _array[i] *= val  ;
    return *this;
  }
};
template <typename Val, typename T, int size>
myArray<T,size> operator*(const Val &lhs, myArray<T, size> tmp) {
  tmp *= lhs; 
  return tmp;
}      

关于为什么operator*=应该是成员函数而不是免费友元函数的基本原理,请参阅此链接