实例化: 对类模板写友元函数出现问题? (instantiation)#

前置内容

解决方案 1: 在类内定义友元函数#

请重新阅读 运算符重载及示例 (operator overloading), 尤其注意最佳实践中对于友元函数怎么定义的建议 (当然那里是针对运算符重载说的).

问题#

问题代码#
 1#include <iostream>
 2
 3template <typename T>
 4class Widget {
 5 public:
 6  Widget(T value) : value_(value) {}
 7
 8  friend void function(Widget widget);
 9
10 private:
11  T value_;
12};
13
14template <typename T>
15void function(Widget<T> widget) {
16  std::cout << widget.value_;
17}
18
19int main() {
20  Widget<int> widget(10);
21  function(widget);
22}

这段代码不能通过编译, Visual Studio 会说有 "无法解析的外部符号", 其他软件可能提示类似下面的内容:

1Undefined symbols for architecture arm64:
2  "function(Widget<int>)", referenced from:
3      _main in main.cpp.o
4ld: symbol(s) not found for architecture arm64

问题在于, 在类内声明的友元函数 function 和类外定义的函数模板 function 是两个不同的函数: 我们并没有定义那个友元函数.

写出这样的代码反映出对模板的理解存在偏差.

解释#

类模板不是类: 类模板作为偏正短语, "类" 是修饰语, 而 "模板" 才是中心语.

我们编写了类模板 template <typename T> class Widget 后, 会以 Widget<int> 的形式使用它. 在编译时, 编译器将会从类模板的代码生成对应的类的代码, 因而有了我们所使用的 Widget<int> 类. 而这个从类模板代码生成类代码的过程称为实例化 (instantiation).

 1template <typename T>
 2class Widget {
 3 public:
 4  Widget<T>() {}
 5};
 6
 7int main() {
 8  Widget<int> widget1;     // 实例化为 Widget<int>
 9  Widget<double> widget2;  // 实例化为 Widget<double>
10}

注意到, 我在第 4 行定义构造函数时写明了 Widget<T>. 这是根据实例化可推断出的写法: 类模板最终会根据给它的模板参数实例化, 那么我们在定义类模板时, 自然可以假装我们在写 Widget<T>.

此外, 类模板最终 会根据它的模板参数实例化, 因此即便我们在类内不写明 Widget<T> 而写 Widget, 也是在指最终通过实例化得到的类 Widget<T>:

 1template <typename T>
 2class Widget {
 3 public:
 4  // ↓ 这也是指最终用模板参数 T 实例化得到的 Widget<T>
 5  Widget() {}
 6};
 7
 8int main() {
 9  Widget<int> widget1;     // 第 5 行指的是 Widget<int>
10  Widget<double> widget2;  // 第 5 行指的是 Widget<double>
11}

由此回顾之前的问题代码, 将模板参数 T 显式写出来:

 1template <typename T>
 2class Widget {
 3 public:
 4  Widget<T>(T value) : value_(value) {}
 5
 6  friend void function(Widget<T> widget);
 7
 8 private:
 9  T value_;
10};

我们并不是让一个函数模板作为友元函数, 而是 分别地,

  • void function(Widget<int> widget) 作为 Widget<int> 的友元函数;

  • void function(Widget<double> widget) 作为 Widget<double> 的友元函数!

然而我们之前是尝试怎样定义它们? 我们以为它们是一个函数模板, 在类外以函数模板的形式定义它们:

1// 这不是 Widget 里声明的友元函数!
2template <typename T>
3void function(Widget<T> widget) {
4  std::cout << widget.value_;
5}

"等等, 如果它不是我所声明的友元函数, 为什么我在它里面用私用数据成员 widget.value_ 没有报错?" 因为你没有实例化这个函数模板. 前面说过, 我们使用模板时, 是由编译器通过模板实例化出了我们实际的代码, 那么既然我们没使用这个模板, 编译器又何必费力去实例化它呢?

这是件坏事

这意味着我们很难知道模板出错了. 甚至哪怕模板被实例化, 由于它不得不发生在编译晚期, 软件很难在不编译的情况下就检测出它的错误.

这更是件好事

这意味着我们无需为自己没有使用的代码付出代价.

它还使模板有了应用的可能. 例如, 还记得我们为什么要进行运算符重载吗? 一个原因是为了让自定义类型支持加减乘除:

1template <typename T>
2T square(T value) {
3  return value * value;
4}

如果编译器贪婪地检查所有模板, 我们将不能定义任何模板.

此外, 虽然不同的类型 TU 都可以由类模板 Widget 实例化为 Widget<T>Widget<U>, 但实例化得到的类 Widget<T>Widget<U> 之间并无关系: 它们的实例化过程是独立地使用不同组模板参数, 且类模板代码里也没有定义它们之间的联系.

那该如何定义它们之间的联系呢? 我们现在的问题是, template <typename T> class Widget 只在类里有 Widget<T> 这一个类, 而我们需要在类里用 Widget<U> 表达另一个类进而让 Widget<T>Widget<U> 之间产生联系. 也就是说, 我们需要在定义 Widget<T> 时引入另一组模板参数 U 到类中.

这太绕了! 但别害怕. 前面说过, "类模板最终会根据给它的模板参数实例化, 那么我们在定义类模板时, 自然可以假装我们在写 Widget<T>". 换而言之, 我们可以假装 Widget<T> 已经实例化出来了, 叫做 class Other, 而我们需要在 Other 类中引入一组模板参数 U 用于表达 OtherWidget<U> 之间的联系.

我们可以用函数模板:

 1template <typename T>
 2class Widget;
 3
 4//    ↓ 假设这就是我们的 Widget<T>
 5class Other {
 6 public:
 7  template <typename U>
 8  void add(Other lhs, Widget<U> rhs) {
 9    /* ... */
10  }
11};

现在把 class Other 替换回 class Widget<T>.

 1template <typename T>
 2class Widget {
 3 public:
 4  template <typename U>
 5  void add(Widget<T> lhs, Widget<U> rhs) {
 6    /* ... */
 7  }
 8};
 9
10int main() {
11  Widget<int> widget1;
12  Widget<double> widget2;
13
14  add(widget1, widget1);  // 正确
15  add(widget1, widget2);  // 正确
16  add(widget2, widget1);  // 正确
17  add(widget2, widget2);  // 正确
18}

解决方案 2: 让友元函数是函数模板#

由此有了问题的第二个解决方案: 让友元函数是函数模板.

 1#include <iostream>
 2
 3template <typename T>
 4class Widget {
 5 public:
 6  Widget(T value) : value_(value) {}
 7
 8  template <typename U>
 9  friend void function(Widget<U> widget);
10
11 private:
12  T value_;
13};
14
15template <typename T>
16void function(Widget<T> widget) {
17  std::cout << widget.value_;
18}
19
20int main() {
21  Widget<int> widget(10);
22  function(widget);
23}

不算解决方案的解决方案: 定义每个友元函数#

既然问题中的 Widget<int>Widget<double> 等是分别以 void function(Widget<int>)void function(Widget<double>) 等为友元函数, 那我们当然能分别定义它们:

问题代码#
 1#include <iostream>
 2
 3template <typename T>
 4class Widget {
 5 public:
 6  Widget(T value) : value_(value) {}
 7
 8  friend void function(Widget widget);
 9
10 private:
11  T value_;
12};
13
14void function(Widget<int> widget) {
15  std::cout << widget.value_;
16}
17
18int main() {
19  Widget<int> widget(10);
20  function(widget);
21}