注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

还东国的博客

行之苟有恒,久久自芬芳

 
 
 

日志

 
 

C++小知识39——模板的分离编译  

2013-04-24 11:33:20|  分类: C++(VC)编程 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
C++小知识39——模板的分离编译
大家都知道,一般C++的编译器在编译工程时,.h文件和.cpp文件中,后者是一个独立的编译单元,在前面的博文也提到过,.h文件会在编译期拷贝到相应的.cpp文件中,然后形成obj文件,编译完成后(这就是分享编译的过程),再由Linker把这些独立的obj链接在一起,生成EXE文件.如果在这个过程中,由于名称或者地址的问题,找不到相应的函数或者变量,就会出现链接错误.(具体的编译链接过程,请参考相应的编译书籍,如编译器和链接器,编译原理以及linux内核修炼之道等书籍,这里不再详细说明).
在Boost库中,有好多的.h .hpp文件,其实这就是为了更好的支持模板的原因,因为目前几乎所有的常用的编译器对模板的分享编译都支持的不是很好,常用的VC和G++几乎是不支持的.也就是说,必须把大家原来已经养成的良好的分离头文件和源文件的风格重新再改回来,写在一起去.这样几乎对很多人来说,是很不习惯的.但这也是没有办法的办法.

先看一个例子,然后再分析:
test.h
template <class T> 
class A
{
public:
int f();

};

template <class T> 
class B
{
public:
int f();

};

template<class T>
int B<T>::f()
{
return 0;
}
template <class T> 
class C
{
public:
int f();

};

test.hpp
template <class T> 
int C<T>::f()
{
return 0;
}

test.cpp
#include "stdafx.h"
#include "test.h"

template <class T>
int A<T>::f()
{
return 0;
}
A<int> t;
void Test()
{
t.f();
}

main.cpp:

void main()
{
A<int> tt;
tt.f();

B<int> m;
m.f();

C<int> c;
c.f();
}
分三种情况进行了模拟:
第一种:只有test.h,test.cpp(下面的实例调用注释)和main.cpp,而且其中只有A这个模板类,其它类注释掉,然后链接器会报一个错误:error LNK2019: 无法解析的外部符号 "public: int __thiscall A<int>::f(void)" (?f@?$A@H@@QAEHXZ),该符号在函数 _wmain 中被引用1>D:\vs2010_project\testMaro\Debug\testMaro.exe : fatal error LNK1120: 1 个无法解析的外部命令
第二种:与第一种一样,但是把test.cpp中的注释解梦.编译成功.
第三种:在test.h中,将B模板类增加,并且在相同文件中实现其模板类,即类的声明和实现都在test.h文件中,见上面的test.h 文件.其它相同,在main.cpp中增加B模板类的实例.结果编译成功.
第四种:增加test.hpp文件,在test.h中实现C模板类,同时在 test.hpp中实现C模板类的实现,然后在main.cpp中增加对test.hpp的引用(当然,也可以在test.h文件中引用,相似的都可以):编译成功.

一开始说过,分离编译将CPP做一个独立单元,但是因为C++的标准上提到:如果一个模板不被用到,就不会被编译,在test.obj中,就没有模板类中二进制代码,也就是f这个函数,但在编译的过程中只是做为一个符号标记或者说地址而已,具体的链接编译器大手一扔,给了链接器,这个傻子小接过来后,便在每个OBJ中打听这些标记或者地址.可惜的是,没找到,没办法只好报第一种错误.
但是如果在test.cpp中使用了这个模板,编译器就会调用他,并且实现他,也就是第二种情况,那么就没问题了.然后B模板类都写在了头文件中,编译的时候都被拷到了main.cpp中,由于特化了这个模板类,所以得到编译,那么这个也通过了.最后一种情况也是如此,只不过是变相的写到了一个文件中.
那么大家就应该很明白了.


函数的模板同样也有类似的问题:
其下转自:http://blog.csdn.net/sky101010ws/article/details/6688185 强烈感谢一下.
C++函数模板的编译方式

目前支持函数模板分离编译的编译器相当稀少,VC和g++都不支持。
模板的文件组织两种方法(<<The C++ programming language>>):
第一种 包含编译方式:
实现和声明放在一个编译单位中,这样这个模板定义所依靠的一些东西就被带到了包含这个编译单位的文件里。坏处是:增大编译器的处理信息量,而且用户会意外的依赖了原本只是为定义这个模板而引进的东西。解决方案:使用名字空间、避免宏定义、以及一般性的减少包含进来的信息量,可以减小这种危害影响。

第二种 分离编译模式:
这种处理就和一般的非inline函数处理一样。不过这里需要用到export关键字了,export关键字的作用就是告诉编译器这个东西别的编译单位可以访问;否则按照一般规则,任何地方使用模板,都要求其定义在作用域中。这种方法的优点是:思路清晰。缺点:因为模板被单独编译了,因此在需要时候,找到合适的定义成了实现者(编译器的责任),要实现者在需要时候找到唯一的一个版本,想想编译器这家伙可就头疼了,因此很少编译器做到这一点。

第三种 显式实例化声明:
标准C++提供了显式实例化声明来帮助程序员控制模板实例化发生的时间。在显式实例化声明中,关键字template后面是函数模板实例的声明,其中显式地指定了模板实参。下例中提供了sum(int* , int)的显式实例化声明:

template<class Type> Type sum( Type op1, int op2 ) {} // 函数模板sum的定义必须给出
// 显式实例化声明
template int* sum< int* >( int*, int );   
    
    该显式实例化声明要求用模板实参int*实例化模板sum()。对于给定的函数模板实例,显式实例化声明在一个程序中只能出现一次。
总结了上面的两种情况后,我们可以看到,第一种在编译时候,任何需要这个模板的地方都引用了这个模板定义,因此编译器据此来优化,去掉冗余的版本。第二种方法就给实现者(编译器)增加了负担,不是去掉冗余的版本,而是在需要时找到这个唯一版本。

函数模板实例化:如果一个函数模板在好几个文件中都有实例化调用,那么我们就称这每一个实例化调用处为模板的一个实例化点。在这若干个实例化点处,对于同一种实例化调用,编译器都有可能进行实例化。一般来说编译器会在这种实例化所有的点中只选择一个实例化点处进行实例化,其他点处的调用都使用该点实例化后的具体函数定义。但是一些编译器(大部分都是老式的编译器)会在每一个实例化点处都进行实例化,生成一个具体的函数定义,然后在这些实例化后的具体定义中选择一个作为这种实例化调用最终的实例化具体定义,其他的具体定义视而不见。这样做的效果虽然同先选择一个实例化点再进行实例化,然后其他实例化调用处使用该点定义的方式一样,但是会使得编译的效率极差,而且随着程序的修改,如果该模板这种实例化调用用的越来越多,那么实例化出来的被视而不见的定义就越来越多,程序的体积就越来越大,越来越臃肿。因此C++标准中加入了显式实例化,使得程序编写者可以通过显式实例化声明来通知编译器:该在此处进行这种实例化,别的地方就不用了
。以避免前面那种因编译器不够智能带来的问题。

正所谓,万仞之山,起于垒土.

  评论这张
 
阅读(1226)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017