设为首页 加入收藏

TOP

1.3.2 链接器如何识别重复模板实例
2013-10-07 16:30:59 来源: 作者: 【 】 浏览:189
Tags:1.3.2 链接 如何 识别 重复 模板 实例

1.3.2 链接器如何识别重复模板实例

假设将例1.6中函数模板func的实现也放在头文件func.hpp中,并且文件caller.cpp及main.cpp中各有函数caller及main都调用func生成实例func<int>,易知编译后的目标文件caller.o及main.o中各自都有func<int>实例。两个函数由同一模板生成,完全等价,则这两个函数为重复模板实例。

如果在最后链接步骤中不做特殊处理,则会在最终目标代码中存在多个等价的模板实例,造成目标文件尺寸的增加,尤其是在大量用到模板库时,这种情况会愈发严重。对此问题,C++(www.cppentry.com)标准中给出的解决方案是:在链接时识别及合并等价的模板实例。

那么,链接器如何识别等价的模板实例呢?答案见例1.7。

例1.7

  1. // ======================================  
  2. // 文件名caller1.cpp  
  3. #include <iostream> 
  4.  
  5. template<typename T> 
  6. void func(T const &v)  
  7. {  
  8.     std::cout << "func1: " << v << std::endl;  
  9. }  
  10.  
  11. void caller1() {  
  12.     func(1);  
  13.     func(0.1);  
  14. }  
  15.  
  16. // ======================================  
  17. // 文件名caller2.cpp  
  18. #include <iostream> 
  19.  
  20. template<typename T> 
  21. void func(T const &v)  
  22. {  
  23.     std::cout << "func2: " << v << std::endl;  
  24. }  
  25.  
  26. void caller2() {  
  27.     func(2);  
  28.     func(0.2f);  
  29. }  
  30.  
  31. // ======================================  
  32. // 文件名main.cpp  
  33. void caller1();  
  34. void caller2();  
  35.  
  36. int main()  
  37. {  
  38.     caller1();  
  39.     caller2();  
  40.     return 0;  

例1.7中用到3个代码文件。其中caller1.cpp和caller2.cpp中都有一个名为func的函数模板,且两个同名模板的模板参数也相同,都只有一个类型模板参数。但两个函数模板内容不同,区别在于打印出的前导字符串。此外,caller1.cpp和caller2.cpp中还分别声明两个函数caller1及caller2,其中都用到各自文件的func模板生成函数实例并调用。

细看代码便知,caller1.cpp编译所得目标文件中有func<int>及func<double>两个函数模板实例,而caller2.cpp编译所得目标文件中有func<int>及func<float>两个函数模板实例。这两个目标文件再与main.cpp编译所得目标文件共同链接成可执行文件后会出现什么情况呢?还是以笔者所用GCC编译器为例,如果用以下命令行编译:

  1. $ g++ caller1.o caller2.o main.o -o a.out 

执行程序的输出如下:

  1. $ ./a.out  
  2. func1: 1  
  3. func1: 0.1  
  4. func1: 2  
  5. func2: 0.2 

很有趣,在函数caller2()中本意是调用caller2.cpp中的func<int>,所以应该输出“func2: 2”。但是由于caller1.cpp与caller2.cpp中均有func<int>实例,并且函数参数列表也相同(都为空),那么在链接时链接器基于函数名、模板实参列表以及参数列表判断两个函数模板实例等价,而将caller2.cpp中的func<int>除名。所有func<int>的调用都被链接到caller1.cpp中的func<int>实例。所以在以上程序输出第三行才会打印“func1: 2”。而caller1()和caller2()中还分别调用了func<double>(无修饰浮点常数默认是double型)及func<int>。由于模板参数类型不同,这是两个不同的函数。链接器在链接时可以区分二者而做出如我们所想的链接。由此例的运行结果可以推知,链接器不考虑函数具体内容,仅仅通过函数名、模板实参列表以及参数列表等“接口”信息来判断两个函数是否等价。

实际上,编译器在编译函数模板实例时,将根据函数名、函数参数类型以及模板参数值等信息来重命名编译所生成的目标函数名,这一处理方式称为Name-Mangling。如果发现“接口”等价的函数(即编译后的函数名相同),则在最终可执行代码中只保留等价函数之一作为链接候选,而放弃其他等价函数。具体保留哪个函数是随机的,可能与用户输入有关。

比如在写链接命令时,将caller2.o放在caller1.o之前,如下所示:

  1. $ g++ caller2.o caller1.o main.o -o a.out.2 

程序运行结果会变为如下所示:

  1. $ ./a.out.2  
  2. func2: 1  
  3. func1: 0.1  
  4. func2: 2  
  5. func2: 0.2 

显然,因为命令行中文件顺序的关系,导致caller2.o中的func<int>先出现,而使得caller1.o中的func<int>实例被编译器放弃。

通常情况下,根据函数接口判断等价函数实例并在链接时合并的简单方法,可以有效解决重复模板实例的问题。但正如例1.7中所演示那样,使用这种方法也有弊端。倘若有不同的作者在写不同的模板库时,碰巧用到同一函数名以及相同的模板参数列表和函数形参列表,对于一些简单函数,这也是非常有可能的。又碰巧两个模板库用在同一项目的不同代码文件之中,则在最终链接时,有可能因为链接器的去重复功能而导致意外的链接结果,使得最终程序工作异常。降低落入这一陷阱的可能性,最好的方法就是避免使用相同的函数名。此时,C++(www.cppentry.com)中的命名空间(namespace)机制就显得异常重要。

模板库作者最好为自己的作品起一个独特的名字,并将所有模板库代码放在此命名空间内,例如所有的C++(www.cppentry.com)标准模板库代码都放在std命名空间内。即使名字很长,库的用户也可以通过为空间改名或者利用using语句显示引用所需函数等办法来降低代码量。只要两个库的命名空间不一样,库中的函数名就不会重复。除非用户采用以下方式强行将两库命名空间内的所有元素引入自己的空间,人为地制造命名冲突:

  1. using namespace libA;  
  2. using namespace libB; 

因此,无论是库开发者还是用户,管理命名的习惯至关重要。这不仅为了提高代码可读性,更是关系到编译结果是否正确。
 

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇1.3.1 HPP文件还是CPP文件 下一篇1.4 尴尬的Export Template

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: