c++模板类

理解编译器的编译模板过程

如何组织编写模板程序

前言
常遇到询问使用模板到底是否容易的问题,我的回答是:“模板的使用是容易的,但组织编写却不容易”。看看我们几乎每天都能遇到的模板类吧,如STL, ATL, WTL, 以及Boost的模板类,都能体会到这样的滋味:接口简单,操作复杂。

我在5年前开始使用模板,那时我看到了MFC的容器类。直到去年我还没有必要自己编写模板类。可是在我需要自己编写模板类时,我首先遇到的事实却是“传统”编程方法(在*.h文件声明,在*.cpp文件中定义)不能用于模板。于是我花费一些时间来了解问题所在及其解决方法。

本文对象是那些熟悉模板但还没有很多编写模板经验的程序员。本文只涉及模板类,未涉及模板函数。但论述的原则对于二者是一样的。

问题的产生
通过下例来说明问题。例如在array.h文件中有模板类array:
// array.h
template <typename T, int SIZE>
class array
{
    T data_[SIZE];
    array (const array& other);
    const array& operator = (const array& other);
public:
    array(){};
    T& operator[](int i) {return data_[i];}
    const T& get_elem (int i) const {return data_[i];}
    void set_elem(int i, const T& value) {data_[i] = value;}
    operator T*() {return data_;}      
};            
            
然后在main.cpp文件中的主函数中使用上述模板:
// main.cpp
#include "array.h"

int main(void)
{
array<int, 50> intArray;
intArray.set_elem(0, 2);
int firstElem = intArray.get_elem(0);
int* begin = intArray;
}
        
这时编译和运行都是正常的。程序先创建一个含有50个整数的数组,然后设置数组的第一个元素值为2,再读取第一个元素值,最后将指针指向数组起点。

但如果用传统编程方式来编写会发生什么事呢?我们来看看:

将array.h文件分裂成为array.h和array.cpp二个文件(main.cpp保持不变)
// array.h        
template <typename T, int SIZE>
class array
{
      T data_[SIZE];
      array (const array& other);
      const array& operator = (const array& other);
  public:
      array(){};
      T& operator[](int i);
      const T& get_elem (int i) const;
      void set_elem(int i, const T& value);
      operator T*();      
};        
    
// array.cpp
#include "array.h"

template<typename T, int SIZE> T& array<T, SIZE>::operator [](int i)
    {
    return data_[i];
    }

template<typename T, int SIZE> const T& array<T, SIZE>::get_elem(int i) const
    {
    return data_[i];
    }

template<typename T, int SIZE> void array<T, SIZE>::set_elem(int i, const T& value)
    {
    data_[i] = value;
    }
template<typename T, int SIZE> array<T, SIZE>::operator T*()
    {
    return data_;
    }
        
编译时会出现3个错误。问题出来了:
  为什么错误都出现在第一个地方?
  为什么只有3个链接出错?array.cpp中有4个成员函数。
  
要回答上面的问题,就要深入了解模板的实例化过程。

模板实例化
程序员在使用模板类时最常犯的错误是将模板类视为某种数据类型。所谓类型参量化(parameterized types)这样的术语导致了这种误解。模板当然不是数据类型,模板就是模板,恰如其名:

编译器使用模板,通过更换模板参数来创建数据类型。这个过程就是模板实例化(Instantiation)。
  从模板类创建得到的类型称之为特例(specialization)。 
  模板实例化取决于编译器能够找到可用代码来创建特例(称之为实例化要素,
  point of instantiation)。
  要创建特例,编译器不但要看到模板的声明,还要看到模板的定义。
  模板实例化过程是迟钝的,即只能用函数的定义来实现实例化。

再回头看上面的例子,可以知道array是一个模板,array<int, 50>是一个模板实例 - 一个类型。从array创建array<int, 50>的过程就是实例化过程。实例化要素体现在main.cpp文件中。如果按照传统方式,编译器在array.h文件中看到了模板的声明,但没有模板的定义,这样编译器就不能创建类型array<int, 50>。但这时并不出错,因为编译器认为模板定义在其它文件中,就把问题留给链接程序处理。

现在,编译array.cpp时会发生什么问题呢?编译器可以解析模板定义并检查语法,但不能生成成员函数的代码。它无法生成代码,因为要生成代码,需要知道模板参数,即需要一个类型,而不是模板本身。

这样,链接程序在main.cpp 或 array.cpp中都找不到array<int, 50>的定义,于是报出无定义成员的错误。

至此,我们回答了第一个问题。但还有第二个问题,在array.cpp中有4个成员函数,链接器为什么只报了3个错误?回答是:实例化的惰性导致这种现象。在main.cpp中还没有用上operator[],编译器还没有实例化它的定义。

解决方法
认识了问题,就能够解决问题:
  在实例化要素中让编译器看到模板定义。
  用另外的文件来显式地实例化类型,这样链接器就能看到该类型。
  使用export关键字。

前二种方法通常称为包含模式,第三种方法则称为分离模式。

第一种方法意味着在使用模板的转换文件中不但要包含模板声明文件,还要包含模板定义文件。在上例中,就是第一个示例,在array.h中用行内函数定义了所有的成员函数。或者在main.cpp文件中也包含进array.cpp文件。这样编译器就能看到模板的声明和定义,并由此生成array<int, 50>实例。这样做的缺点是编译文件会变得很大,显然要降低编译和链接速度。

第二种方法,通过显式的模板实例化得到类型。最好将所有的显式实例化过程安放在另外的文件中。在本例中,可以创建一个新文件templateinstantiations.cpp:
// templateinstantiations.cpp                
#include "array.cpp"

template class array <int, 50>; // 显式实例化
        
array<int, 50>类型不是在main.cpp中产生,而是在templateinstantiations.cpp中产生。这样链接器就能够找到它的定义。用这种方法,不会产生巨大的头文件,加快编译速度。而且头文件本身也显得更加“干净”和更具有可读性。但这个方法不能得到惰性实例化的好处,即它将显式地生成所有的成员函数。另外还要维护templateinstantiations.cpp文件。

第三种方法是在模板定义中使用export关键字,剩下的事就让编译器去自行处理了。当我在
Stroustrup的书中读到export时,感到非常兴奋。但很快就发现VC 6.0不支持它,后来又发现根本没有编译器能够支持这个关键字(第一个支持它的编译器要在2002年底才问世)。自那以后,我阅读了不少关于export的文章,了解到它几乎不能解决用包含模式能够解决的问题。欲知更多的export关键字,建议读读Herb Sutter撰写的文章。

结论
要开发模板库,就要知道模板类不是所谓的"原始类型",要用其它的编程思路。本文目的不是要吓唬那些想进行模板编程的程序员。恰恰相反,是要提醒他们避免犯下开始模板编程时都会出现的错误。

//////////////////////////////
http://www.cnblogs.com/xgchang/archive/2004/11/12/63139.aspx
甚至是在定义非内联函数时,模板的头文件中也会放置所有的声明和定义。这似乎违背了通常的头文件规则:“不要在分配存储空间前放置任何东西”,这条规则是为了防止在连接时的多重定义错误。但模板定义很特殊。由template<...>处理的任何东西都意味着编译器在当时不为它分配存储空间,它一直出于等待状态直到被一个模板实例告知。在编译器和连接器的某一处,有一机制能去掉模板的多重定义,所以为了容易使用,几乎总是在头文件中放置全部的模板声明和定义。

为什么C++编译器不能支持对模板的分离式编译 
刘未鹏(pongba) /文

首先,C++标准中提到,一个编译单元[translation unit]是指一个.cpp文件以及它所include的所有.h文件,.h文件里的代码将会被扩展到包含它的.cpp文件里,然后编译器编译该.cpp文件为一个.obj文件,后者拥有PE[Portable Executable,即windows可执行文件]文件格式,并且本身包含的就已经是二进制码,但是,不一定能够执行,因为并不保证其中一定有main函数。当编译器将一个工程里的所有.cpp文件以分离的方式编译完毕后,再由连接器(linker)进行连接成为一个.exe文件。 
举个例子: 
//---------------test.h-------------------// 
void f();//这里声明一个函数f 
//---------------test.cpp--------------// 
#include”test.h” 
void f() 

…//do something 
} //这里实现出test.h中声明的f函数 
//---------------main.cpp--------------// 
#include”test.h” 
int main() 

f(); //调用f,f具有外部连接类型 

在这个例子中,test. cpp和main.cpp各被编译成为不同的.obj文件[姑且命名为test.obj和main.obj],在main.cpp中,调用了f函数,然而当编译器编译main.cpp时,它所仅仅知道的只是main.cpp中所包含的test.h文件中的一个关于void f();的声明,所以,编译器将这里的f看作外部连接类型,即认为它的函数实现代码在另一个.obj文件中,本例也就是test.obj,也就是说,main.obj中实际没有关于f函数的哪怕一行二进制代码,而这些代码实际存在于test.cpp所编译成的test.obj中。在main.obj中对f的调用只会生成一行call指令,像这样: 
call f [C++中这个名字当然是经过mangling[处理]过的] 
在编译时,这个call指令显然是错误的,因为main.obj中并无一行f的实现代码。那怎么办呢?这就是连接器的任务,连接器负责在其它的.obj中[本例为test.obj]寻找f的实现代码,找到以后将call f这个指令的调用地址换成实际的f的函数进入点地址。需要注意的是:连接器实际上将工程里的.obj“连接”成了一个.exe文件,而它最关键的任务就是上面说的,寻找一个外部连接符号在另一个.obj中的地址,然后替换原来的“虚假”地址。 
这个过程如果说的更深入就是: 
call f这行指令其实并不是这样的,它实际上是所谓的stub,也就是一个 
jmp 0x23423[这个地址可能是任意的,然而关键是这个地址上有一行指令来进行真正的call f动作。也就是说,这个.obj文件里面所有对f的调用都jmp向同一个地址,在后者那儿才真正”call”f。这样做的好处就是连接器修改地址时只要对后者的call XXX地址作改动就行了。但是,连接器是如何找到f的实际地址的呢[在本例中这处于test.obj中],因为.obj于.exe的格式都是一样的,在这样的文件中有一个符号导入表和符号导出表[import table和export table]其中将所有符号和它们的地址关联起来。这样连接器只要在test.obj的符号导出表中寻找符号f[当然C++对f作了mangling]的地址就行了,然后作一些偏移量处理后[因为是将两个.obj文件合并,当然地址会有一定的偏移,这个连接器清楚]写入main.obj中的符号导入表中f所占有的那一项。 
这就是大概的过程。其中关键就是: 
编译main.cpp时,编译器不知道f的实现,所有当碰到对它的调用时只是给出一个指示,指示连接器应该为它寻找f的实现体。这也就是说main.obj中没有关于f的任何一行二进制代码。 
编译test.cpp时,编译器找到了f的实现。于是乎f的实现[二进制代码]出现在test.obj里。 
连接时,连接器在test.obj中找到f的实现代码[二进制]的地址[通过符号导出表]。然后将main.obj中悬而未决的call XXX地址改成f实际的地址。 
完成。

然而,对于模板,你知道,模板函数的代码其实并不能直接编译成二进制代码,其中要有一个“具现化”的过程。举个例子: 
//----------main.cpp------// 
template<class T> 
void f(T t) 
{} 
int main() 

…//do something 
f(10); //call f<int> 编译器在这里决定给f一个f<int>的具现体 
…//do other thing 

也就是说,如果你在main.cpp文件中没有调用过f,f也就得不到具现,从而main.obj中也就没有关于f的任意一行二进制代码!!如果你这样调用了: 
f(10); //f<int>得以具现化出来 
f(10.0); //f<double>得以具现化出来 
这样main.obj中也就有了f<int>,f<double>两个函数的二进制代码段。以此类推。 
然而具现化要求编译器知道模板的定义,不是吗? 
看下面的例子:[将模板和它的实现分离] 
//-------------test.h----------------// 
template<class T> 
class A 

public: 
void f(); //这里只是个声明 
}; 
//---------------test.cpp-------------// 
#include”test.h” 
template<class T> 
void A<T>::f() //模板的实现,但注意:不是具现 

…//do something 

//---------------main.cpp---------------// 
#include”test.h” 
int main() 

A<int> a; 
a. f(); //编译器在这里并不知道A<int>::f的定义,因为它不在test.h里面 
//于是编译器只好寄希望于连接器,希望它能够在其他.obj里面找到 
//A<int>::f的实现体,在本例中就是test.obj,然而,后者中真有A<int>::f的 
//二进制代码吗?NO!!!因为C++标准明确表示,当一个模板不被用到的时 
//侯它就不该被具现出来,test.cpp中用到了A<int>::f了吗?没有!!所以实 
//际上test.cpp编译出来的test.obj文件中关于A::f的一行二进制代码也没有 
//于是连接器就傻眼了,只好给出一个连接错误 
//但是,如果在test.cpp中写一个函数,其中调用A<int>::f,则编译器会将其//具现出来,因为在这个点上[test.cpp中],编译器知道模板的定义,所以能//够具现化,于是,test.obj的符号导出表中就有了A<int>::f这个符号的地 
//址,于是连接器就能够完成任务。 
}

关键是:在分离式编译的环境下,编译器编译某一个.cpp文件时并不知道另一个.cpp文件的存在,也不会去查找[当遇到未决符号时它会寄希望于连接器]。这种模式在没有模板的情况下运行良好,但遇到模板时就傻眼了,因为模板仅在需要的时候才会具现化出来,所以,当编译器只看到模板的声明时,它不能具现化该模板,只能创建一个具有外部连接的符号并期待连接器能够将符号的地址决议出来。然而当实现该模板的.cpp文件中没有用到模板的具现体时,编译器懒得去具现,所以,整个工程的.obj中就找不到一行模板具现体的二进制代码,于是连接器也黔

/////////////////////////////////
http://dev.csdn.net/develop/article/19/19587.shtm
 C++模板代码的组织方式 ——包含模式(Inclusion Model)     选择自 sam1111 的 Blog  
关键字   Template Inclusion Model 
出处   C++ Template: The Complete Guide

说明:本文译自《C++ Template: The Complete Guide》一书的第6章中的部分内容。最近看到C++论坛上常有关于模板的包含模式的帖子,联想到自己初学模板时,也为类似的问题困惑过,因此翻译此文,希望对初学者有所帮助。

模板代码有几种不同的组织方式,本文介绍其中最流行的一种方式:包含模式。

链接错误

大多数C/C++程序员向下面这样组织他们的非模板代码:

·类和其他类型全部放在头文件中,这些头文件具有.hpp(或者.H, .h, .hh, .hxx)扩展名。

·对于全局变量和(非内联)函数,只有声明放在头文件中,而定义放在点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>这样的头文件也包含了和我们类似的模板定义。

这在实践中确实是一个问题,因为它增加了编译器在编译一个实际程序时所需的时间。我们因此会在以后的章节中验证其他一些可能的方法来解决这个问题。但无论如何,现实世界中的程序花一小时来编译链接已经是快的了(我们曾经遇到过花费数天时间来从源码编译的程序)。

抛开编译时间不谈,我们强烈建议如果可能尽量按照包含模式组织模板代码。

另一个观察结果是,非内联模板函数与内联函数和宏的最重要的不同在于:它并不会在调用端展开。相反,当模板函数被具现化时,会产生此函数的一个新的拷贝。由于这是一个自动的过程,编译器也许会在不同的文件中产生两个相同的拷贝,从而引起链接器报告一个错误。理论上,我们并不关心这一点:这是编译器设计者应当关心的事情。实际上,大多数时候一切都运转正常,我们根本就不用处理这种状况。然而,对于那些需要创建自己的库的大型项目,这个问题偶尔会显现出来。

最后,需要指出的是,在我们的例子中,应用于普通模板函数的方法同样适用于模板类的成员函数和静态数据成员,以及模板成员函数。

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/look01/archive/2008/11/05/3228134.aspx

 

c++模板类的更多相关文章

  1. 单链表的C++实现(采用模板类)

    采用模板类实现的好处是,不用拘泥于特定的数据类型.就像活字印刷术,制定好模板,就可以批量印刷,比手抄要强多少倍! 此处不具体介绍泛型编程,还是着重叙述链表的定义和相关操作.  链表结构定义 定义单链表 ...

  2. 模板类 error LNK2019: 无法解析的外部符号

    如果将类模板的声明和实现写在两个独立的文件中,在构建时会出现"error LNK2019: 无法解析的外部符号 "的错误. 解决方法有: 第一种方法,就是把类模板中成员函数的声明和 ...

  3. 关于g++编译模板类的问题

    今天搞了我接近4个小时,代码没错,就是调试没有通过,无论怎么也没有想到是编译器的问题 g++不支持c++模板类 声明与实现分离,都要写到.h文件里面. 以后记住了.

  4. C++11特性(模板类 initializer_list)

    [1]initializer_list模板类 C++primer 原文如下: 通读原文相关篇幅,分析解读内容如下: 提供initializer_list类的初衷,为了便于将有限个同一类型(或可转换为同 ...

  5. C++11模板类使用心得

    1.推荐使用std::shared_ptr<TaskT>代替指针TaskT*使用,shared_ptr是一种智能指针,能自主销毁释放内存,在c++11中被引入,在多线程编程中有很大的用处, ...

  6. C++ 模板函数与模板类

    一.模板函数 函数模板提供了一类函数的抽象,即代表了一类函数.当函数模板被实例化后,它会生成具体的模板函数.例如下面便是一个函数模板:

  7. 模板类重载<<运算符

    写了一个Matrix模板类,需要重载<<, 1.需要友元函数 2.需要此函数的实现在.h中(本人试验出来的,放在.cpp中编译不通过) template <typename T> ...

  8. C++模板类的使用

    1.定义模板类 通过类似于下面的语法可以定义一个模板类: template<typename T> class Job : public virtual RefBase { public: ...

  9. C++模板编程里的主版本模板类、全特化、偏特化(C++ Type Traits)

    1.  主版本模板类 首先我们来看一段初学者都能看懂,应用了模板的程序: 1 #include <iostream> 2 using namespace std; 3 4 template ...

随机推荐

  1. iOS开发小技巧--即时通讯项目:使用富文本在UILabel中显示图片和文字;使用富文本占位显示图片

    Label借助富文本显示图片 1.即时通讯项目中语音消息UI的实现,样式如图: 借助富文本在UILabel中显示图片和文字 // 1.创建一个可变的富文本 NSMutableAttributedStr ...

  2. 【POJ 1228】Grandpa's Estate 凸包

    找到凸包后暴力枚举边进行$check$,注意凸包是一条线(或者说两条线)的情况要输出$NO$ #include<cmath> #include<cstdio> #include ...

  3. Entity Framework Code First (七)空间数据类型 Spatial Data Types

    声明:本文针对 EF5+, Visual Studio 2012+ 空间数据类型(Spatial Data Types)是在 EF5 中引入的,空间数据类型表现有两种: Geography (地理学上 ...

  4. Windows Server 2008 R2 每隔一段时间自动关机解决办法

    情况描述: “我的电脑-->右键属性”中显示“已激活”,而“管理工具”中显示未激活.系统中有进程wlms.exe. 网上找了下解决方式: 1.提权工具:PSTOOLS(下载地址:http://m ...

  5. yum命令mysql,jdk,tomcat

    一.首先使用Yum list available xxx命令罗列出远程仓库里面可以安装的包 1.列出可以安装的java包 yum list available java* 输出如下: 2.列出可以安装 ...

  6. js-函数eval

    eval函数接收一个参数s,如果s不是字符串,则直接返回s.否则执行s语句.如果s语句执行结果是一个值,则返回此值,否则返回undefined. 需要特别注意的是对象声明语法“{}”并不能返回一个值, ...

  7. js 时间与时间戳的转换

      一:时间转时间戳:javascript获得时间戳的方法有四种,都是通过实例化时间对象 new Date() 来进一步获取当前的时间戳 1.var timestamp1 = Date.parse(n ...

  8. Android studio:从Eclipse迁移到Android Studio【一】

    转载:http://www.apkbus.com/forum.php?mod=viewthread&tid=255061&extra=page%3D2%26filter%3Dautho ...

  9. js-JavaScript高级程序设计学习笔记15

    第十七章 错误处理与调试 1.try-catch语句.如果try块中的任何代码发生了错误,就会立即退出执行开始执行catch块.必须要给catch的错误对象起名字.不同浏览器都有一个message属性 ...

  10. spring-HelloWorld

    准备工作,安装SpringIDE,找到SpringSource-tool-suit,然后按照, 关键的一部在与找到之后该安转那些呢,答案是只安转带有SpringIDE的, 有四个,且不要点击联网进行更 ...