C++中模板类的编译过程

为什么未实例化的模板类编译器不编译呢?

首先要明白,C++中每一个对象所占的空间大小,对象的内存分布都是在编译时期就确定下来的。而对于模板类来说,对象占空间的大小和内存分布是不知道的,依所套用的类型而定,比如A为模板类,则A<int>类对象所占的空间大小和内存分布显然不同于A<double>。(这里插一句,虽然模板类中有一个类字,但是对于实例化的模板类才算是真正的类,未实例化的模板类还不能算是类。)因此,对于未实例化的模板类,编译器无法确定其大小,所以略过对模板类的编译,在编译时只检查一些与模板无关的错误。而此时如果模板类的声明和定义中有错误的话,编译器就检查不到。

模板的错误报告

首先看一个来自《深度探索C++对象模型》中的例子,在vs2010下编译

template <class T>
class Mumble
{
public$ :
	Mumble(T t = 1024) :_t(t)
	{
		tt = 1023;
		if (tt != t)
			throw std::exception("wrong answer") std::exception("wrong answer")
	}

	static void doMumble();
	void hehe();
private:
	T tt;
}


显然编译错误,两个明显的错误是public标示符打错以及类的声明未以分号结束。现在把这两个错误更改,如下

template <class T>
class Mumble
{
public :
	Mumble(T t = 1024) :_t(t)
	{
		tt = 1023;
		if (tt != t)
			throw std::exception("wrong answer") std::exception("wrong answer")
	}

	static void doMumble();
	void hehe();
private:
	T tt;
};


这时,编译成功。(请注意,这时我并未实例化Mumble)。其实这个类中依然还有很多错误,如下

  1. t以一个整型变量1024初始化,这时问题就来了,t能否以一个整型变量初始化?不知道,依类型T而定。
  2. _t是未定义的data member,但是这个错误没有检查出来。
  3. T类是否定义了“!=”运算法呢?也不知道,依类型T而定。
  4. throw语句明显也是错误的,不允许一个标示符后面紧跟一个标示符,且该语句未以分号结束。

这些错误未被检查出来的原因,正如上面所说的,编译器不会去编译未实例化模板类中的代码,因此不会发现错误。其实编译器不去编译未实例化模板类是可以理解的,一个原因就像第一段所说的,编译器无法确定未实例化的模板类的空间大小和内存布局,第二个原因,就是代码中某些行为的合法性依赖于模板类套用的类型,比如上面例子中的T,如果T为int,那么错误1和错误3就是合法的。现在我们来实例化一个模板类

int main()
{
	Mumble<int> mumble;
	return 0;
}

编译,于是错误2,4出现了


现在,我把错误更新过来,更改过后的模板类如下:

template <class T>
class Mumble
{
public :
	Mumble(T t = 1024)
	{
		//tt = 1023;
		if (tt != t)
			throw std::exception("wrong answer");
	}

	static void doMumble();
	void hehe();
private:
	T tt;
};

现在我来证明,模板类中行为的合法性有时候依赖于套用的类型

实验1

int main()
{
	Mumble<int*> mumble;
	return 0;
}

编译再次错误,


原因就是无法用整型变量1024去初始化一个int*变量。

 

实验2

class  SmallInt
{
public:
	SmallInt(int t = 0);

	SmallInt(const SmallInt& rhs) {
		st = rhs.st;
	};
private:
	int st;
};
int main()
{
	Mumble<SmallInt> mumble;
	return 0;
}

这时,我定义了一个类SmallInt,这个类中支持用一个整型变量初始化,但未提供“!=”运算符,编译,果然错了,错误如下



说了这么多,其实只是为了说明,编译器不会编译未实例化的模板类,因此会漏检一些错误。

同一个模板类套用不同的类型就属于不同的类

标题有点绕口,意思就是,比如Mumble<int>和Mumble<float>是两个不同的类。

我们还是用实验来说明问题,在Mumble类中有一个static的函数成员doMumble,验证着两个类是否为同一个类的最好方法就是看这两个类中的doMumble函数是否具有相同的地址,实验如下

int main()
{
	Mumble<int> mumble1;
	Mumble<float> mumble2;
	Mumble<int> mumble3;

	std::cout << &mumble1.doMumble << "\n";
	std::cout << &mumble2.doMumble << "\n";
	std::cout << &mumble3.doMumble << "\n";

	return 0;
}

结果:


可见Mumble<int>和Mumble<float>中的doMumble具有不同的地址,是两个不同的类。

模板类的实现和声明不能分离编译

用过模板类的同学都知道,当模板类的声明和实现放在不同的文件中(比如声明在.h文件,实现在.cpp文件),编译实例化的模板类时会发生链接错误,提示无法解析的外部命令。为什么呢?为什么对于非模板类和其他非模板函数分离编译是成功的?

举个例子说明

test.h文件:
                 void f();  // 函数的声明
test.cpp文件:
                 void f() {...} // 函数的实现
main.cpp文件:
                 #include“test.h”
                 int main()
                 {
                        f();
                 }

先从编译讲起,一个编译单元是一个cpp文件和它所include的所有头文件,编译时,头文件中的代码会拓展进cpp文件中,生成一个obj文件(一个cpp文件就是一个obj文件),obj文件中包含的就是二进制代码。例子中会生成两个obj文件:test.obj和main.obj文件,test.obj中含有f函数的二进制代码,main.obj中含有函数main的二进制代码。由于main.cpp中只包含了test.h的头文件,即只有f的声明,所以main.obj中没有函数f的二进制码,这时编译器无法在调用f时,展开f的二进制码,因此编译器将其看成是外部链接类型,认为f的二进制码在另一个obj文件中,f的调用需要有一个指向f二进制码所在的地址,main.obj中先给出一个虚假地址(因为这时还不知道真实的地址)。

从链接来说,链接就是把多个obj文件生成成一个执行文件,即exe文件。例子中,链接器在test.obj文件中找到了f的二进制码,然后把该二进制码所在的地址给了main.obj中,将虚假地址替换,这时main就能顺利调用了。

而对于模板类,同样举个例子说明

test.h文件:
               template<class T>
               class A
               {
                public:
                        void f();
               }
 test.cpp文件:
               #include "test.h"
               template<class T>
               void A<T>::f()
               {
                        ...... 
               }
main.cpp文件: 
               #include "test.h" 
               int main() 
               { 
                      A<int> a; 
                      a.f(); 
                      return 0;
               }

同样的,在编译时,会生成两个obj文件,在main.obj文件中找不到A<int>::f的实现,将其看做外部链接类型,需要在链接的时候从其他obj文件中找到A<int>::f的二进制码,将其所在的地址给main.obj文件。这时问题就出现了,在链接的时候,连接器在test.obj中找不到A<int>::f的实现(因为A没有被实例化),链接失败,发出了“无法解析的外部命令”错误。因此,模板类的声明和实现都放在.h文件中就能解决了。

其实模板类的声明和实现时可以分离编译的,在test.cpp中对模板类进行实例化,如下

test.cpp文件:
               #include "test.h"
               template<class T>
               void A<T>::f()
               {
                        ...... 
               }
               template class A<int>;
这时就不会出现链接错误的情况了,但是这个方法还是有缺陷的,因为你不知道类的使用方会套用什么类型,你需要对每个类型在cpp文件中挨个实例化一次,这就很麻烦,尤其是使用方套用的类型是自己定义了一个类的话,还是会出现链接错误。


版权声明:本文为u011201045原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。