12.4. 模板友元工厂实现

我们所需要的是,为每一个用于实例化类模板 C 的 T 类型,自动生成一个非模板函数。当然,事先生成所有这些函数是不可能的 —— 理论上,可以与模板类 C 一起使用的 T 类型几乎无限制。我们并不需要为所有这些类型都生成 operator+() 函数 —— 只需要为程序中实际使用了该模板的类型生成即可。

12.4.1 按需生成友元函数

我们即将看到的这种模式非常古老,它由 John Barton 和 Lee Nackman 于 1994 年提出,最初是为了解决当时编译器的某些限制。发明者将其命名为“受限模板展开”,但这个名称从未广泛使用。多年后,Dan Sacks 将其命名为“友元工厂”,但这种模式有时也简单地称为 Barton-Nackman 技巧。

这种模式看起来非常简单,与本章前面的代码非常相似:

// Example 17
template <typename T> class C {
  int x_;
public:
  C(int x) : x_(x) {}
  friend C operator+(const C& lhs, const C& rhs) {
    return C(lhs.x_ + rhs.x_);
  }
}

我们正在利用一个非常特定的 C++ 特性,代码必须精确编写。一个非模板的友元函数在类模板内部定义。这个函数必须是内联定义的,不能只是在类内声明为友元,然后在外部再定义,除非作为显式的模板实例化 —— 本可以在类内声明友元函数,然后定义 operator+<const C<int>&, const C<int>&>,但这只对 C<int> 有效,对 C<double> 无效(因为不知道调用者之后可能会实例化哪些类型,所以这并不太有用)。该函数的参数可以是模板参数 T 的类型、C<T> 类型(在类模板内部可以简单地称为 C),以及其他固定类型或仅依赖于模板参数的类型,但它本身不能是一个模板。每次使用组合模板参数类型对 C 类模板进行实例化时,都会恰好生成一个具有指定名称的非模板、非成员函数。生成的函数是非模板函数,其是普通函数,适用于它们的是一般的类型转换规则。现在,我们又回到了非模板的 operator+(),所有转换都按照我们期望的方式工作:

C<int> x(1), y(2);
C<int> z1 = x + y; // 可以工作
C<int> z2 = x + 2; // 也可以工作
C<int> z3 = 1 + 2; // 亦可以工作

这就是整个模式,需要注意几个细节。首先,关键字 friend 不能省略。一个类通常无法生成非成员函数,除非是通过声明一个友元函数。即使该函数不需要访问私有数据,为了能够从类模板的实例化中自动产生非模板函数,这些函数也必须声明为友元(静态非成员函数也可以通过类似方式生成,但二元运算符不能是静态函数 —— 标准明确禁止了)。其次,生成的函数可放置在包含该类的作用域中,但必须通过参数依赖查找(ADL)才能找到,正如本章前面所了解到的。例如,为 C 模板类定义插入运算符,但在那之前,先将整个类包装在一个命名空间中:

// Example 18
namespace NS {
  template <typename T> class C {
    int x_;
  public:
    C(int x) : x_(x) {}

    friend C operator+(const C& lhs, const C& rhs) {
      return C(lhs.x_ + rhs.x_);
    }

    friend std::ostream&
    operator<<(std::ostream& out, const C& c) {
      out << c.x_;
      return out;
    }
  };
} // namespace NS

现在,既可以相加也可以打印 C 类型的对象:

NS::C<int> x(1), y(2);
std::cout << (x + y) << std::endl;

尽管 C 类模板现在位于命名空间 NS 中,必须以 NS::C<int> 的形式使用,但调用 operator+() 或 operator<<() 时,并不需要做特殊处理,但这并不意味着这些函数是在全局作用域中生成的。不,它们仍然在 NS 命名空间中,但我们所看到的是参数依赖查找(ADL)在起作用 —— 例如,当查找名为 operator+() 的函数时,编译器会考虑当前作用域(即全局作用域,而那里没有相关函数)中的候选函数,以及函数参数类型所定义的作用域中的候选函数。在我们的例子中,operator+() 的至少一个参数是 NS::C<int> 类型,这会自动将 NS 命名空间中声明的所有函数纳入考虑范围。友元工厂模式在包含类模板的作用域(当然也是 NS 命名空间)中生成函数。因此,查找过程找到了定义,+ 和 << 操作都能解析为我们所期望的方式。这是设计使然,并非偶然;参数查找规则经过精心调整,以产生这种理想且符合预期的结果。

很容易证明,友元函数是在包含类的作用域(在我们的例子中是命名空间 NS)中生成的,但它们只能通过参数依赖查找(ADL)才能找到。如果不使用参数依赖查找,直接尝试查找该函数将会失败:

auto p = &NS::C<int>::operator+; // 无法进行编译

友元工厂模式与我们之前研究过的某个模式之间也存在联系。