关于C++为什么模板类中的模板函数只能写在头文件

关于C++为什么模板类中的模板函数只能写在头文件

那究竟是什么原因造成模板函数不能放在cpp文件里定义呢?本文就围绕着这个问题进行探究。

从编码到运行

首先笔者想要探讨一下一个C++项目是如何从文本文件生成可执行文件的。

预编译

预编译是将预编译宏命令进行处理,如#include, #define#ifdef,#pragma等。

编译

编译是将文本中的源码转换成计算机能直接处理的汇编语言。这里要注意的是C++采用的是分离式编译(各个的h、cpp文件的编译独立)。

c++开发中广泛使用声明和实现分开的开发形式,其编译过程是分离式编译,就是说各个cpp文件完全分开编译,然后生成各自的obj目标文件,最后通过连接器link生成一个可执行的exe文件。引用自:https://blog.csdn.net/uestclr/article/details/51372780

链接

编译中提到了C++采用的是分离是编译,例如如下代码:

Test.h

void test();
Test.cpp

#include "Test.h"
void test() {
    //todo
}
main.cpp

#include "test.h"
int main() {
    test();
    return 0;
}

到此为止,编译期间应生成两个文件:(macOS下)

  • Test.cpp.o
  • main.cpp.o

编译期间,main.cpp调用test()函数时并不知道test()函数的函数入口地址,编译器会将该函数符号(test())记录到符号导入表(因为 .a为文件与可执行文件的格式是一样的,在这样的文件中有一个符号导入表和符号导出表(import table 和 export table)其中将所有符号和它们的地址关联起来)。

这样连接器只要在 Test.cpp.o 的符号导出表中寻找符号 test()的地址就行了,然后作一些偏移量处理后(因为是将两个.o 文件合并,当然地址会有一定的偏移,这个连接器清楚)写入 main.cpp.o 中的符号导入表中test() 所占有的那一项即可。

模板函数

回到开头所探讨的话题,模板函数声明与定义写在两个文件中,会在编译器链接的时候报错,具体例子如下:

Test.h

template<class T, T t>
void test(T a);
Test.cpp

#include "Test.h"
template<class T, T t>
void test(T a) {
    //todo
}
main.cpp

#include "Test.h"
int main() {
    test<int, 1>();
    return 0;
}

编译器报错:

[ 18%] Linking CXX executable cpp_learning
Undefined symbols for architecture x86_64:
  "void test<int, 1>(int)", referenced from:
      main() in main.cpp.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

看以判断出链接阶段报错。

为什么?

关于模板函数的限制

C++标准明确表示,当一个模板不被用到的时侯,它就不该被实例化出来。这就表示Test.cpp.o中没有test函数的定义。

在分离式编译的环境下,编译器编译某一个 .cpp 文件时并不知道另一个 .cpp 文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器)。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会实例化出来,所以,当编译器只看到模板的声明时,它不能实例化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的 .cpp 文件中没有用到模板的实例时,编译器懒得去实例化,所以,整个工程的 .a 中就找不到一行模板实例的二进制代码,于是连接器也黔驴技穷了。 ----引用:https://www.jianshu.com/p/dc94f0cbfcf7

解决方法

方法如下:

  1. 将模板函数定义写在h文件中;
  2. 在cpp文件中使用模板函数(这样就能在编译时实例化模板函数)