制作函数模板静态库

C++模板的学习会遇到各种各样的问题,对于一个某种程度上的新手而言,难免会碰到一些问题。但C++模板呈现出的泛型编程思维拥有着“双拳敌四手”的绝妙心法,威风八面,实在也让自己按捺不住。前些天自己一次对reverse模板的实现过程让自己体会到认真去琢磨一些小问题也会有一些意想不到的收获,所以如今每每遇到问题就会尝试着去探个究竟并把过程记录下来,有时候自觉也陷落于诸多语法的细枝末节当中,好在学习模板当前仅是一项技术积累,并无工程进度要求,暂且细细为之。
 
1. 起因
equal, search, find, find_if, copy, remove_copy, remove_copy_if, remove, transform, partition, accumulate,这些都是STL中的算法成员,自己一个一个予以实现并且进行简单的测试。在测试的过程当中,每次总是要去编写一个函数模板专门用来打印容器当中的元素,于是便想到是否可以把它也做成一个静态库,或者高级点,制作成一个动态库。 
 1 #include <iostream>
 2 #include <vector>
 3  
 4 template <class IN> // 这一部分做成静态库
 5 void PrintByIterator(IN begin, IN end)
 6 {
 7     while (begin != end)
 8     {
 9         std::cout <<*begin <<" ";
10         ++begin;
11     }
12     std::cout <<std::endl;
13 }
14  
15 int main()
16 {
17     int testArr[5] = {1, 3, 5, 9, 2}; 
18     PrintByIterator(testArr, testArr + 5);
19     return 0;
20 }

为了便于笔记主题的清晰演示,上面给出了一段代码示例,通过它来演示本笔记所要达成的制作函数模板静态库的目的。如上代码片段当中的函数模板PrintByIterator,如果能够将其制作成为一个静态库,就可以避免在实现其他函数的同时重新编写或者复制拷贝的工作。当然,将一些函数模板或者类模板制作为库想必也存在有其它需求场景。 

2. 方案探索
按照库的制作流程,我将测试工程的代码重新进行了组织,主要思路是按照常见非函数模板的静态库制作流程,分为两部分:
  • 静态库部分:将PrintByIterator模板的定义单独新建静态库工程
  • 原工程部分:通过头文件以接口的方式对PrintByIterator进行调用
 1 // 静态库工程,包含PrintByIterator模板定义
 2 
 3 #include <iostream>
 4 template <class IN>
 5 void PrintByIterator(IN begin, IN end)
 6 {
 7     while (begin != end)
 8     {
 9         std::cout <<*begin <<" ";
10         ++begin;
11     }
12     std::cout <<std::endl;
13 }
 1 // 测试工程 - cprint.h
 2 
 3 #ifndef __CPRINT_H_
 4 #define __CPRINT_H_
 5  
 6 template <class IN>
 7 void PrintByIterator(IN begin, IN end);
 8  
 9 #endif // __CPRINT_H_
10  
11  
12 // 测试工程 - main.cpp
13 #include <iostream>
14 #include "cprint.h"
15 int main()
16 {
17     int testArr[5] = {1, 3, 5, 9, 2};
18     PrintByIterator(testArr, testArr + 5);
19     return 0;
20 }

然而,如上的代码组织方式,在编译工程的时候会出现如下的错误提示:

||=== Build: Debug in TemplateLibraryTest (compiler: GNU GCC Compiler) ===|
objDebugmain.o||In function `main':|
F:CodingC++Chapter8 TemplateTemplateLibraryTestmain.cpp|8|undefined reference to `void PrintByIterator<int*>(int*, int*)'|
||=== Build failed: 1 error(s), 0 warning(s) (0 minute(s), 0 second(s)) ===|
 
该编译错误的信息说明,当前工程在链接阶段找不到针对PrintByIterator针对int *的实例化版本。在当时,我没有想到是否是模板的编译本身的原因,而是去猜想该错误是否是因为静态库的制作与使用出现了问题。因此,我便再次将库的相关知识学习了一番,直到发现关键的问题在何处,下面对整个问题的推演与剥离过程简要进行描述。 

2.1 重温“静态库”

对于静态库和动态库的制作,这里有一篇介绍颇为详细的笔记。静态库的优点之一也就是可以复用代码,提高开发效率。不过,它的优点远不止于此:
  • 一种生态环境的组件:通常操作系统会通过库的形式提供API
  • 复用代码:库通常是对一些常见功能的封装,比如编译器携带的运行时库
  • 设计上的考虑:通过静态库的组织形式可以将函数的定义与实现相分离,在合适条件下对于库的使用者隐藏具体实现
  • 利于项目管理:通过将模块化,除了可以进行权限管理之外,还可以通过库的组织形式提高工程build效率
 如上,静态库给我们提供了诸多的好处,但依然有着优化的空间。比如可执行文件当中的多个相同拷贝、库更新时候需要整个工程重新编译。这个时候,产生了动态库技术,它很好的弥补了静态库的这些不足:
  • 没有静态库bin/exe文件空间浪费的缺点
  • 不需要在库更新之后整个工程重新编译和发布
  • 具有静态库的优点
 静态库的构建过程简略如下:
 制作函数模板静态库
 
可见,静态库是在链接阶段进行链接。回到上面的问题,对比看起来如上的做法似乎并无不对的地方。如果按照通常的函数静态库对提示的错误进行类比推测:错误出现在链接阶段,也就是说明了在自己制作的动态库当中根本没有包含有对应的目标代码。那是不是因为模板的编译与通常函数的编译有不一样的地方呢? 

2.2 模板编译的特殊性

编译模板函数与编译非模板函数的确有不一样的地方,标准C++为编译模板代码定义了两种模型:包含编译模型与分离编译模型。
  • 包含编译模型
在包含编译模型当中,编译器必须看到所有模板的定义。一般而言,可以通过在声明函数模板的头文件中添加一条#include指令将模板定义包含到头文件中。
  • 分离编译模型
在分离编译模型中,必须启用export关键字来跟踪相关的模板定义。export关键字能够指明给定的定义可能会需要在其他文件中产生实例化。
编译器对如上编译模型的实现各有不同,当前而言几乎所有的编译器都支持包含编译模型,部分编译器支持分离编译模型。同时的,每一种编译器在处理模板实例化时的优化方式均各有不同。基于这两点,必要时只有去查阅编译器的用户指南去了解个究竟。对于分离编译模型,自己当前使用的CodeBlocks 13.12并不支持,无法识别关键字export。
制作函数模板静态库
制作函数模板静态库
CodeBlocks13.12 版本不支持export关键字
 
模板本身是C++中一个非常好的特性,而编译器对其的支持决定了它的与众不同——模板定义与声明不能分开放置。对于函数模板来说,编译器看到模板定义的时候,它不立即产生代码。只有在看到使用函数模板时,编译器才产生特定类型的模板实例,相当于生产对应类型的代码。也就是,在上面的静态库构建图示当中,模板的实例化过程可以抽象为在“编译”阶段完成。所以它也被称为编译期多态
 
相对于一般函数调用而言,编译器只需要看到函数的定义,就会直接编译对应的代码,之后在链接阶段将其与引用它的目标代码链接到一起。对于函数模板,在编译时,编译器看到函数模板的定义时根本不会生成对应的代码,直到看到使用函数模板时才开始进行类型推导并根据具体的类型生成具体的实例函数,比如生成对应的compare<int>(), compare<double>代码。这个推演函数模板类型,再实例化对应类型的函数过程也就是通常笼统的被称为模板实例化的过程。
 
上面这种将两个过程合成为实例化过程的想法,容易让我们形成一种想法:“编译函数模板时必须要见到函数模板的定义。”如果抱有这种想法,那么也就意味着为函数模板制作静态库是不可能的。我自己当时也是如此以为,直到查找到一份PPT资料,才把类型推演与具体类型函数生成两个过程区分开来,这也就可以让静态库的制作成为可能。
 
3 解决方案
有了之前的分析过程,便可大致了解模板的整个构建过程,可以得知最为关键的问题也就是:在链接阶段提供具体类型函数的那部分代码。
 

方式一:

 1 // cprint.h
 2 #ifndef __CPRINT_H_
 3 #define __CPRINT_H_
 4 template <class IN>
 5 void PrintByIterator(IN begin, IN end)
 6 {
 7     while (begin != end)
 8     {
 9         std::cout <<*begin <<" ";
10         ++begin;
11     }
12     std::cout <<std::endl;
13 }
14 #endif // __CPRINT_H_
15 
16 // main.cpp
17 #include <iostream>
18 #include <vector>
19 #include "cprint.h"
20 
21 int main()
22 {    
23     int testArr[5] = {1, 3, 5, 9, 2};
24     PrintByIterator(testArr, testArr + 5); 
25     
26     return 0;
27 }

这是第一种方式,也即是将函数模板的实现放在头文件当中。这个时候类型的推演和int *类型函数的生成可以统一被视作一个“实例化”的过程。

方式二:

 1 // cprint.h
 2 #ifndef __CPRINT_H_
 3 #define __CPRINT_H_
 4 
 5 #include "cprint.cpp"
 6 
 7 #endif // __CPRINT_H_
 8 
 9 // cprint.cpp
10 #include <iostream>
11 
12 template <class IN>
13 void PrintByIterator(IN begin, IN end)
14 {
15     while (begin != end)
16     {
17         std::cout <<*begin <<" ";
18         ++begin;
19     }
20     std::cout <<std::endl;
21 }
22 
23 // main.cpp
24 #include <iostream>
25 #include <vector>
26 #include "cprint.h"
27 
28 int main()
29 {
30     int testArr[5] = {1, 3, 5, 9, 2};
31     PrintByIterator(testArr, testArr + 5);
32     return 0;
33 }

这种方式在头文件当中包含函数模板的实现文件,与第一种方式实质是一样的,也就是对于函数模板的使用者来说,它可以同时看见函数的声明与定义。

方式三:

 1 // cprint_ins.cpp
 2 #include <vector>
 3 #include "cprint.cpp"
 4 
 5 using std::vector;
 6 
 7 template void PrintByIterator<int *>(int * a, int * b);
 8 
 9 
10 // cprint.cpp
11 #include <iostream>
12 
13 template <class IN>
14 void PrintByIterator(IN begin, IN end)
15 {
16     while (begin != end)
17     {
18         std::cout <<*begin <<" ";
19         ++begin;
20     }
21     std::cout <<std::endl;
22 }
 
 1 // cprint.h
 2 #ifndef __CPRINT_H_
 3 #define __CPRINT_H_
 4 
 5 template <class IN>
 6 void PrintByIterator(IN begin, IN end);
 7 
 8 #endif // __CPRINT_H_
 9 
10 // main.cpp
11 #include <iostream>
12 #include <vector>
13 #include "cprint.h"
14 
15 int main()
16 {
17     int testArr[5] = {1, 3, 5, 9, 2};
18     PrintByIterator(testArr, testArr + 5);
19     return 0;
20 }

这种方式开始看起来比较怪异,但也是本文最终所选定的最终方案,通过对它们的合理组织,便可以使得在链接阶段提供针对int *类型函数的实例化代码。其实现是通过在cprint_ins.cpp当中将模板PrintByIterator进行了显示的实例化操作,所以可以将它们作为库的形式提供,在链接阶段也就可以提供针对int *的实例化版本。如此便可使得静态库的制作得以实现。

<完>