cpp模板类定义放在头文件原因

编译单元

编译单元是指一个源文件(.c/.cpp)文件和包含展开的头文件,之后编译器会将该文件编译成一个 object 文件(在 linux 中是 .o 文件)。

1
gcc xxx.cc -c

之后用可以看见当前目录下生成一个 xxx.o 的文件,这个文件就是 xxx.cc 编译单元生成的对应二进制。这个二进制里面有该文件所对应的符号。

普通成员函数

现在有如下例子,尝试编译对应的二进制文件,不链接:

main.cc

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
a.print();
}

A.h:

1
2
3
4
5
class A {
public:
int m;
void print();
};

A.cc

1
2
3
4
5
6
7
#include <iostream>
#include "A.h"
using namespace std;

void A::print() {
cout << m << endl;
}

之后可以用 nm 命令查看二进制中的符号,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@└────> # g++ main.cc -c
@└────> # g++ A.cc -c
@└────> # nm main.o
0000000000000000 T main
U _ZN1A5printEv
@└────> # nm A.o
U __cxa_atexit
U __dso_handle
000000000000006c t _GLOBAL__sub_I__ZN1A5printEv
000000000000002e t _Z41__static_initialization_and_destruction_0ii
0000000000000000 T _ZN1A5printEv
U _ZNSolsEi
U _ZNSolsEPFRSoS_E
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _ZSt4cout
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

可以看到,在 main.o 中,有一个 _ZN1A5printEv,是对 undefined,也就是说编译器在这个编译单元中找不到这个符号。在 main.o 中,看下反汇编,调用 print 函数的时候是类似 call print 这种形式跳转函数的。因为找不到这个符号,自然也就不知道这个函数的地址,所以需要链接器链接有这个符号的 A.o,就可以运行了。常规的成员函数这样运行的,那么如果换成模板成员函数会怎么样呢?

模板成员函数

修改代码如下:

main.cc

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
a.print<int>();
}

A.h:

1
2
3
4
5
6
class A {
public:
int m;
template<class T>
void print();
};

A.cc

1
2
3
4
5
6
7
#include <iostream>
#include "A.h"
using namespace std;
template<class T>
void A::print() {
cout << m << endl;
}

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@└────> # g++ main.cc -c
@└────> # g++ A.cc -c
@└────> # nm main.o
0000000000000000 T main
U _ZN1A5printIiEEvv
@└────> # nm A.o
U __cxa_atexit
U __dso_handle
000000000000003e t _GLOBAL__sub_I_A.cc
0000000000000000 t _Z41__static_initialization_and_destruction_0ii
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

可以看到,在换成模板函数后,A.o 中根本没有 print 的符号!尽管 A.cc 中有定义。这是因为在 C++ 中,当一个模板不被用到的时侯它就不该被实例化出来,而在 A.cc 中显然这个模板没有被用到。所以编译器在编译 main.o 的时候,没找到符号,就把希望寄托于链接器。而链接器在链接的时候也懵逼了,因为这个符号在 A.o 中也没有。所以这种情况下链接器会报找不到符号的错误。

解决方案

所以回到主题,这就是为什么对于 C++ 的模板函数来说,要把模板函数的实现放到头文件中。

main.cc

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
a.print<int>();
}

A.h:

1
2
3
4
5
6
7
8
9
#include <iostream>
class A {
public:
int m;
template<class T>
void print() {
std::cout << m << std::endl;
}
};

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@└────> # g++ main.cc -c
@└────> # nm main.o
U __cxa_atexit
U __dso_handle
0000000000000059 t _GLOBAL__sub_I_main
0000000000000000 T main
000000000000001b t _Z41__static_initialization_and_destruction_0ii
0000000000000000 W _ZN1A5printIiEEvv
U _ZNSolsEi
U _ZNSolsEPFRSoS_E
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _ZSt4cout
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

可以看到 main.o 中包含 print 的符号。