编译器的编译模板过程

转载 2013年12月04日 14:26:45

c++模板类(一)理解编译器的编译模板过程

分类: c++ 模板类 4622人阅读 评论(6) 收藏 举报
 

如何组织编写模板程序 

前言
常遇到询问使用模板到底是否容易的问题,我的回答是:“模板的使用是容易的,但组织编写却不容易”。看看我们几乎每天都能遇到的模板类吧,如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撰写的文章。

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

 

编译器的编译基本过程

编译器最基本的功能就是把高级语言(例如C/Fortran)编写的代码转化为机器指令(就是01串),从这个角度来说它本质上是个转换过程。经典的编译过程主要包括: 1、词法分析(Lexical Ana...
  • doniexun
  • doniexun
  • 2014年07月31日 17:56
  • 2059

c++模板类(一)理解编译器的编译模板过程

c++模板类(一)理解编译器的编译模板过程   如何组织编写模板程序  前言 常遇到询问使用模板到底是否容易的问题,我的回答是:“模板的使用是容易的,但组织编写却不容易”。看看我们几乎每天...
  • OnafioO
  • OnafioO
  • 2014年06月10日 16:41
  • 3412

Xcode 中的编译过程以及编译器

转自:https://segmentfault.com/a/1190000003101087 基本的编译过程分为四个步骤: 预处理(Pre-process):把宏替换,删除注释,展开头...
  • zhh152
  • zhh152
  • 2016年11月09日 11:34
  • 786

C++ 编译器处理模板的原理

编译器遇到模板方法定义时,会进行语法检查,但是并不编译模板。编译器无法编译模板定义,因为它不知道使用什么类型。不知道x和y的类型,编译器就无法为x=y这样的语句生成代码。 编译器遇到一个实例化的模板时...
  • u011206291
  • u011206291
  • 2016年10月26日 10:19
  • 1173

代码变成可执行程序期间,编译器做了那些事?

代码变成可执行程序期间,编译器做了那些事? 1. 怎么就在编译时确定了 sizeof 的大小了? 2.编译时确定的东西? 3.运行时确定的东西?      ...
  • u014774781
  • u014774781
  • 2015年09月20日 11:10
  • 1069

iOS编译过程的原理和应用

前言 一般可以将编程语言分为两种,编译语言和直译式语言。 像C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率...
  • Hello_Hwc
  • Hello_Hwc
  • 2016年12月10日 11:58
  • 15919

JVM编译器的编译过程

一、编译器概述 本片文章叙述的是HotSpot虚拟机的编译过程,HotSpot包含一个解释器javac和两个即时编译器,它们之间配合工作。 解释器与编译器: 1、解释器与编译器两者各有优势,当...
  • tingfeng96
  • tingfeng96
  • 2016年08月20日 15:21
  • 1766

Java代码编译过程简述

Javac编译器,能将一种语言规范转化成另外一种语言规范,通常编译器都是将便于人理解的语言规范转化成机器容易理解的语言规范,如C/C++或者汇编语言都是将源代码直接编译成目标机器码,这个目标机器代码是...
  • fuzhongmin05
  • fuzhongmin05
  • 2017年02月05日 11:31
  • 1345

iOS编译过程的原理和应用

[http://blog.csdn.net/Hello_Hwc/article/details/53557308] 科普文,转载备用 前言 一般可以将编程语言分为两种,编...
  • guojin08
  • guojin08
  • 2016年12月28日 10:45
  • 728

模板与分离编译模式

一个程序(项目)由若干个源文件共同实现,而每个源文件单独编译生成目标文件,最后将所有目标文件连接起来形成单一的可执行文件的过程成为分离编译模式。2.使用模板在连接时出错在C++程序设计中,在一个源文件...
  • K346K346
  • K346K346
  • 2015年10月30日 00:56
  • 2627
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:编译器的编译模板过程
举报原因:
原因补充:

(最多只允许输入30个字)