12.5. 友元工厂与奇异递归模板模式(CRTP)的结合

友元工厂是一种模式,从类模板的每一次实例化中合成一个非模板、非成员函数 —— 每次模板被实例化为一种新类型时,就会生成一个新的函数。该函数的参数可以使用在该类模板实例化中能够声明的类型。这个类型就是类本身,但也可以是模板所知晓的类型。

以这种方式,友元工厂可以与我们在第8章中研究的模式结合使用。回想一下,CRTP 的主要思想是:一个类从一个基类模板的实例化中继承,而该基类模板的参数正是派生类的类型。考虑一下我们在这里拥有的情况 —— 一个基类模板,会随着每一个从它派生的类而自动实例化,并且知道那个类型是什么。这似乎正是放置友元工厂的理想位置。

当然,由基类生成的运算符不仅需要知道它们所操作对象的类型,还需要知道如何处理该对象(例如,如何打印它)。有时,所需的数据实际上存在于基类中,基类就可以提供完整的实现。但派生类对基类的添加如此之少的情况是罕见的。

CRTP 与友元工厂更常见的结合用法是:通过某种其他功能以标准方式实现某些运算符。例如,operator!=() 可以通过 operator==() 来实现:

// Example 19
template <typename D> class B {
public:
  friend bool operator!=(const D& lhs, const D& rhs) {
    return !(lhs == rhs);
  }
};

template <typename T> class C : public B<C<T>> {
  T x_;
public:
  C(T x) : x_(x) {}
  friend bool operator==(const C& lhs, const C& rhs) {
    return lhs.x_ == rhs.x_;
  }
};

派生类 C 使用友元工厂模式,直接从类模板的实例化中为二元运算符 operator==() 生成一个非模板函数。同时,也继承自基类 B,这也会触发该模板的实例化,进而为每一个我们已经生成了 operator==() 的类型,生成一个对应的 operator!=() 的非模板函数。

CRTP 的第二种用途是将成员函数转换为非成员函数。例如,二元运算符 operator+() 有时会基于 operator+=() 来实现,而 operator+=() 总是一个成员函数(作用于其第一个操作数)。为了实现二元 operator+(),需要有人来处理向该对象类型的转换,然后才能调用 operator+=()。可以通过友元工厂的通用 CRTP 基类所生成的二元运算符,来进行这些转换。

同样地,如果约定类都具有一个 print() 成员函数,那么插入运算符(operator<<)也可以生成:

// Example 20
template <typename D> class B {
public:
  friend D operator+(const D& lhs, const D& rhs) {
    D res(lhs);
    res += rhs; // 将 += 转化为 +
    return res;
  }
  friend std::ostream&
  operator<<(std::ostream& out, const D& d) {
    d.print(out);
    return out;
  }
};

template <typename T> class C : public B<C<T>> {
  T x_;
public:
  C(T x) : x_(x) {}
  C operator+=(const C& incr) {
    x_ += incr.x_;
    return *this;
  }
  void print(std::ostream& out) const {
    out << x_;
  }
};

通过这种方式,CRTP 可以用来添加样板化的接口,同时将具体的实现委托给派生类。毕竟,它本质上是一种静态的(编译时)委托模式。