现在的位置: 首页 > 综合 > 正文

C++模板的编译问题

2013年10月19日 ⁄ 综合 ⁄ 共 3490字 ⁄ 字号 评论关闭

对于模板,包括模板类与模板函数,它们的代码其实并不是直接翻译成二进制代码,它要求有一个“具体化”的过程,举个例子:
template <class T>
void FunA(T t)
{
}

int main()
{
FunA(10); // call FunA(int) 编译器在这里决定给FunA一个FunA(int)的具体实现体。
}

也就是说,如果在main函数中,没有调用过FunA函数的话,那么在main.obj中就找不到关于FunA的任意二进制代码,如果调用了FunA函数,那么在main.obj就会找到关于FunA函数的具体化二进制代码。
然而具体化要求编译器知道模板的定义。看看下面的例子(将模板实现与声明分离):

Test.h文件
template <class T>
class A
{
public:
void FunA(); // 这里只是声明了一个函数
}

Test.cpp文件
#include "Test.h"
template <class T>
void A<T>::FunA()
{
}

main.cpp文件
#include "Test.h"
int main()
{
A<int> a;
a.FunA(); // 编译器在这里并不知道A<int>::FunA函数的定义,因为它不在Test.h文件里面。
}

由于编译器不知道A<int>::FunA函数的定义,那么它就只好希望链接器能够在Test.obj里面找到它的定义了。然而,Test.obj里面真的就有A<int>::FunA的二进制代码吗?答案是没有的,因为根据C++标准,当一个模板不被用到进它就不应该被具体化,也就是说Test.cpp里面如果没有用到A<int>::FunA的话,A<int>::FunA函数的二进制代码就不会被编译到Test.obj文件中去。由于Test.cpp没有调用过A<int>::FunA函数,所以在Test.obj中就不会有A<int>::FunA的二进制代码。于是当链接器进行链接时,它找不到A<int>::FunA的二进制,所以就会给出一个链接错误。但是,当在Test.cpp中写一个函数调用A<int>::FunA函数,那么在Test.obj中就会有A<int>::FunA这个符号地址,于是链接就能够完成。

这个问题要怎么解决呢?
下面就讲一个C++模板代码的组织方式——包含模式。
大多数C/C++程序员向下面这样组织他们的非模板代码:
1)类和其他类型全部放在头文件中,这些头文件具有.hpp(或者.H, .h, .hh, .hxx)扩展名。
2)对于全局变量和(非内联)函数,只有声明放在头文件中,而定义放在点C文件中,这些文件具有.cpp(或者.C, .c, .cc, .cxx)扩展名。
这种组织方式工作的很好:它使得在编程时可以方便地访问所需的类型定义,并且避免了来自链接器的“变量或函数重复定义”的错误。
由于以上组织方式约定的影响,模板编程新手往往会犯一个同样的错误。下面这一小段程序反映了这种错误。就像对待“普通代码”那样,我们在头文件中定义模板:
// basics/myfirst.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
// declaration of template
template <typename T>
void print_typeof (T const&);
#endif // MYFIRST_HPP

print_typeof()声明了一个简单的辅助函数用来打印一些类型信息。函数的定义放在点C文件中:
// basics/myfirst.cpp
#include <iostream>
#include <typeinfo>
#include "myfirst.hpp"

// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{
std::cout << typeid(x).name() << std::endl;
}

这个例子使用typeid操作符来打印一个字符串,这个字符串描述了传入的参数的类型信息。 最后,我们在另外一个点C文件中使用我们的模板,在这个文件中模板声明被#include:
// basics/myfirstmain.cpp
#include "myfirst.hpp"
// use of the template
int main()
{
double ice = 3.0;
print_typeof(ice); // call function template for type double
}

大部分C++编译器(Compiler)很可能会接受这个程序,没有任何问题,但是链接器(Linker)大概会报告一个错误,指出缺少函数print_typeof()的定义。
这个错误的原因在于,模板函数print_typeof()的定义还没有被具现化(instantiate)。为了具现化一个模板,编译器必须知道哪一个定义应该被具现化,以及使用什么样的模板参数来具现化。不幸的是,在前面的例子中,这两组信息存在于分开编译的不同文件中。因此,当我们的编译器看到对print_typeof()的调用,但是没有看到此函数为double类型具现化的定义时,它只是假设这样的定义在别处提供,并且创建一个那个定义的引用(链接器使用此引用解析)。另一方面,当编译器处理myfirst.cpp时,该文件并没有任何指示表明它必须为它所包含的特殊参数具现化模板定义。
头文件中的模板
解决上面这个问题的通用解法是,采用与我们使用宏或者内联函数相同的方法:我们将模板的定义包含进声明模板的头文件中。对于我们的例子,我们可以通过将#include "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一个使用我们的模板的点C文件中包含myfirst.cpp文件,来达到目的。当然,还有第三种方法,就是删掉myfirst.cpp文件,并重写myfirst.hpp文件,使它包含所有的模板声明与定义:

// basics/myfirst2.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP
#include <iostream>
#include <typeinfo>

// declaration of template
template <typename T>
void print_typeof (T const&);
// implementation/definition of template
template <typename T>
void print_typeof (T const& x)
{
std::cout << typeid(x).name() << std::endl;
}
#endif // MYFIRST_HPP

这种组织模板代码的方式就称作包含模式。经过这样的调整,你会发现我们的程序已经能够正确编译、链接、执行了。
从这个方法中我们可以得到一些观察结果。最值得注意的一点是,这个方法在相当程度上增加了包含myfirst.hpp的开销。在这个例子中,这种开销并不是由模板定义自身的尺寸引起的,而是由这样一个事实引起的,即我们必须包含我们的模板用到的头文件,在这个例子中是<iostream>和<typeinfo>。你会发现这最终导致了成千上万行的代码,因为诸如<iostream>这样的头文件也包含了和我们类似的模板定义。
这在实践中确实是一个问题,因为它增加了编译器在编译一个实际程序时所需的时间。我们因此会在以后的章节中验证其他一些可能的方法来解决这个问题。但无论如何,现实世界中的程序花一小时来编译链接已经是快的了(我们曾经遇到过花费数天时间来从源码编译的程序)。
抛开编译时间不谈,我们强烈建议如果可能尽量按照包含模式组织模板代码。
另一个观察结果是,非内联模板函数与内联函数和宏的最重要的不同在于:它并不会在调用端展开。相反,当模板函数被具现化时,会产生此函数的一个新的拷贝。由于这是一个自动的过程,编译器也许会在不同的文件中产生两个相同的拷贝,从而引起链接器报告一个错误。理论上,我们并不关心这一点:这是编译器设计者应当关心的事情。实际上,大多数时候一切都运转正常,我们根本就不用处理这种状况。然而,对于那些需要创建自己的库的大型项目,这个问题偶尔会显现出来。
最后,需要指出的是,在我们的例子中,应用于普通模板函数的方法同样适用于模板类的成员函数和静态数据成员,以及模板成员函数。

 

转自:http://blog.sina.com.cn/s/blog_625cce0801017u31.html

抱歉!评论已关闭.