1.3 如何处理函数模板中的函数体
既然编译器是在需要生成模板实例时自动生成,这就带来一个与传统C/C++(www.cppentry.com)编程(www.cppentry.com)习惯的冲突,即函数模板中的函数体应该放在哪里。
1.3.1 HPP文件还是CPP文件
按照C++(www.cppentry.com)语言习惯,普通函数及类的声明应该放在一个头文件(通常以h、hpp或者hh为扩展名)里,而将其实现放在一个主代码文件(通常以c、cpp或者cc为扩展名)里,这样便于将代码分散编译到多个目标文件中,最后通过链接形成一个完整的目标文件。但是由于模板的实现是随用随生成,并不存在真实的函数实现代码,如果还是按照“头文件放声明,主文件放实现”的做法,则会导致编译失败。
例如一个最简单的模板函数声明如下所示:
- // 文件名func.hpp
- template<typename T>
- T const& func(T const &v);
这个声明放在一个名为“func.hpp”的头文件中,其实现放在名为“func.cpp”的文件中,代码如下所示:
- // 文件名func.cpp
- template<typename T>
- T const& func(T const &v) {return v;}
在一个名为“main.hpp”的文件中定义一个main函数用来调用func函数模板,代码如下所示:
- // 文件名main.cpp
- #include "func.hpp"
- int main() {func(0);}
正如我们通常安排普通函数的代码那样,如果单独编译这两个CPP文件都没有问题,但是在链接两个目标文件时链接器就会报错。在笔者的编译环境下出现的错误如下:
- $ g++ func.o main.o
- main.o: In function 'main':
- main.cpp:(.text+0x17): undefined reference to `int const& func<int>(int const&)'
- collect2: ld 返回 1
链接器报的错误是func<int>即func函数模板的某个实例未定义。按常理,这样一个函数的实现应该是在func.cpp编译出的目标文件中定义。但如果查看该目标文件(笔者用的是func.o)就会发现其中空空如也,并无任何函数定义。
回想一下模板的工作原理就不难理解这一现象。编译器在编译func.cpp时,只是读到了func函数模板的实现,并没有读到任何需要生成函数模板实例的语句,所以不会生成任何func函数的实例。而在编译main.cpp时,虽然用到了一个函数模板实例,但因为main.cpp只是将func.hpp头文件包含进来,而后者只有一个func函数模板的声明,并无具体函数体实现,此时编译器也无法生成func函数模板实例,只好预留一个调用链接,期望在最后的链接过程中可以找到函数实现。但很遗憾这样的实现并不存在,于是最后链接时出错。
稍微修改func.cpp中的代码,使其生成一个func<int>的函数实现,如例1.6所示。
例1.6
- // 文件名func2.cpp
- template<typename T>
- T const& func(T const &v) {return v;}
-
- template int const& func(int const &v);
例1.6中用到一种尚未介绍过的语法——明确生成模板实例。当关键字template后没有模板参数列表,而是一个函数声明时,意味着指示编译器根据此函数声明寻找合适的模板实现。当然,所声明函数必须与某一已知模板函数同名,并且其参数可用模板匹配。
例1.6中将函数声明为T=int,从而在编译func2.cpp时,会在目标文件中生成func<int>的代码而不会在链接时产生错误。但这只是权宜之计,倘若还需要func<float>或者func<char>,那么在代码文件中还得增加相应的语句,以促使编译器生成相应函数模板实例。如此一来,又变成由人工生成模板实例,违背了当初由编译器随用随生成的初衷。
可见,虽然模板中的函数也可以有自己的声明和实现,但编译器不会在读到模板实现时立刻生成实际代码,因为具体的模板参数类型还未知,无法进行编译。对于编译器来说,模板实现也是一种声明,声明如何自动生成代码。所以模板的实现也应该放在头文件内,这样,在其他代码文件中可以直接将模板的实现也包含进来,当需要生成模板实例时,编译器可根据已知模板实现当场生成,而无需依赖在别的目标文件中生成的模板实例。
但这样会带来另一个问题,即重复模板实例。