为什么未实例化的模板类编译器不编译呢?
首先要明白,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)。其实这个类中依然还有很多错误,如下
- t以一个整型变量1024初始化,这时问题就来了,t能否以一个整型变量初始化?不知道,依类型T而定。
- _t是未定义的data member,但是这个错误没有检查出来。
- T类是否定义了“!=”运算法呢?也不知道,依类型T而定。
- 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文件中挨个实例化一次,这就很麻烦,尤其是使用方套用的类型是自己定义了一个类的话,还是会出现链接错误。