实例化: 对类模板写友元函数出现问题? (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}
如果编译器贪婪地检查所有模板, 我们将不能定义任何模板.
此外, 虽然不同的类型 T
和 U
都可以由类模板 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
用于表达 Other
和 Widget<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}