0%

C++: 模板类编译过程中出现“undefined reference to”问题

C++在使用模版(template)类的时候,如果像通常那样将类成员函数的声明和实现分别放在.h.cpp中,会导致在编译时会报错undefined reference to,找不到对应成员函数。

起因

在实现一个模板类的时候遇到了一个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Matrix.h
template<typename T>
class Matrix {
Matrix(vector<vector<T>> &matrix);
void display();
}

// Matrix.cpp
template<typename T>
Matrix<T>::Matrix(vector<vector<T>> &matrix) {
...
}
template<typename T>
void Matrix<T>::display() {
...
}

// main.cpp
Matrix<int> base(vector<vector(int)> m);
base.display();

编译时会报错:

1
2
3
CMakeFiles\AlgorithmWarehouse.dir/objects.a(main.cpp.obj): In function `main':
./main.cpp:75: undefined reference to `Matrix<int>::Matrix(std::vector<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > > >&)'
./main.cpp:76: undefined reference to `Matrix<int>::display()'

很迷不知道为什么会找不到构造函数,查了一圈确定是因为使用template<typename T>的原因,总结一下。

原理

下面使用一个简单的例子来叙述这个问题:假设要实现一个栈,有两个文件Stack.hStack.cpp,栈声明使用template<typename T>main.cppimport "Stack.h"后调用这个类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Stack.h
template<typename T>
class Stack {
void push(...);
}

// Stack.cpp
template<typename T>
void Stack<T>::push() {
...
}

// main.cpp
import "Stack.h"
Stack<int> s;
s.push(...);

编译下来会报错:undefined reference to

因为template其实是一种类似语法糖的东西,C++中每一个对象所占用的空间大小,是在编译的时候就确定的,在模板类没有真正的被使用之前,编译器是无法知道,模板类中使用模板类型的对象的所占用的空间的大小的。只有模板被真正使用的时候,编译器才知道,模板套用的是什么类型,应该分配多少空间。这也就是模板类为什么只是称之为模板,而不是泛型的缘故。

既然是在编译的时候,根据套用的不同类型进行编译,那么,套用不同类型的模板类实际上就是两个不同的类型,也就是说,stack<int>stack<char>是两个不同的数据类型,他们共同的成员函数也不是同一个函数,只不过具有相似的功能罢了。

所以模版本质其实和宏差不多,不同于原本的类型定义。编译器在编译main.cpp时会根据需要隐式实例化诸如 Stack<int>,如果这个实例并没有在main.cpp中,编译器就会根据include去找,但是显然编译时Stack.h中并没有对Stack<int>的声明,于是就报错了。

解决方法

显式声明

Stack.cpp中显式声明会用到的类型:

1
2
template class Stack<int>;
template class Stack<std::string>;

我认为的最好的办法,但缺点也很显然,如果需要一种新的数据类型就很难受,必须手动再添加,封装上不太友好。

全放到头文件中

很直接,把所有Stack.cpp中的源代码全放到Stack.h中,不需要进行任何修改。但缺点就是如果这个头文件在很多文件中被使用的话,会影响编译速度,但不会产生任何链接问题,因为编译器会忽略重复实现。

把实现代码移至一个新头文件中

Stack.cpp改为Stack_impl.h,然后在Stack.h里面import "Stack_impl.h",这样能保证代码实现和声明在不同的文件中,但本质上和上一个解决办法是一样的。