考虑下述程序,它无法正确编译/链接:
main.cpp
#include <iostream>
template <typename T>
T addOne(T x); // 函数模板前向声明
int main()
{
std::cout << addOne(1) << '\n';
std::cout << addOne(2.3) << '\n';
return 0;
}
add.cpp
template <typename T>
T addOne(T x) // 函数模板定义
{
return x + 1;
}
若 addOne
是普通函数,上述写法可行:
- 编译器在
main.cpp
里看到前向声明即可; - 链接器会把
main.cpp
中对addOne()
的调用连接到add.cpp
中的定义。
然而,模板函数并不遵循这一流程,结果得到链接错误:
error LNK2019: unresolved external symbol "int __cdecl addOne<int>(...)"
error LNK2019: unresolved external symbol "double __cdecl addOne<double>(...)"
原因如下:
- 在
main.cpp
中,编译器只看到前向声明,无法看到模板定义,因此不能实例化addOne<int>
和addOne<double>
; - 在
add.cpp
中,虽然有模板定义,但没有任何调用,编译器也不会实例化任何版本; - 链接阶段找不到已实例化的函数,于是报错。
(补充:即使在 add.cpp
里手动实例化所需版本,也只是权宜之计,代码脆弱,不应采用。)
解决之道——将模板完整定义放入头文件
add.h
#ifndef ADD_H
#define ADD_H
template <typename T>
T addOne(T x)
{
return x + 1;
}
#endif
main.cpp
#include "add.h" // 直接包含模板定义
#include <iostream>
int main()
{
std::cout << addOne(1) << '\n'; // 实例化 addOne<int>
std::cout << addOne(2.3) << '\n'; // 实例化 addOne<double>
return 0;
}
- 预处理器把
add.h
内容复制到任何包含它的源文件; - 编译器在每个翻译单元都能看到完整定义,按需实例化所需版本;
- 由于模板定义不受“一次定义规则”(ODR) 的限制,且由模板隐式实例化出的函数均为 inline,因此不会出现重复定义问题。
关键洞察
- 模板定义可安全地包含在多个源文件;
- 由模板实例化得到的函数自动为 inline,可在多个翻译单元中拥有相同定义。
最佳实践
凡需在多个文件中使用的模板,一律将其完整定义放入头文件,并在需要的地方 #include
该头文件。