如果说上一篇博文《模板名称》是教人怎么写模板,那么这一篇就是教人怎么使用模板。
模板实例化的复杂性在于:对于产生自模板的实体,它们的定义已经不再局限于源代码中的单一位置。
一、理解两个概念
(1)实例化:实例化在C++中通常指“根据类型创建一个对象”,但是在模板里面,实例化是指使用具体值替换模板实参,从模板中产生普通类,函数或者成员函数的过程。
(2)特化:这个过程最后获得的试题就是我们所说的特化。
然而,实例化过程并不是特化的唯一方式,还有显式特化,通过引入一个template<>来实现,如下:
template<typename T1, typename T2>
class MyClass{};
template<>
class MyClass<std::string, float>{};
二、按需实例化(on-demand实例化)
如果(某个组件)期望知道模板特化的大小,或者访问该特化的成员,那么整个定义就需要位于作用域中。
比如显示的调用模板的成员,或者是包含隐式转换
(1)显示调用成员函数
template<typename T> class C; //前置声明
C<int>* p = 0; //这里只需要声明就够了
template<typename T>
class C{
public:
void f();
};
void g(C<int>& c){
c.f(); //此处需要知道整个模板的定义,因为编译器要确定f()是不是可以被访问到
}
(2)隐式类型转换
C++重载规则要求:如果候选函数的参数是class类型,那么该类型所对应的类就必须可见
template <typename T>
class C{
public:
C(int); //单参数隐式类型转换
};
void candidate(C<double> const&); //①允许编译器实例化该重载函数,但不是必须的,在VS2013中,就没有实例化参数
void candidate(int){} //②
int main()
{
candidate(42); //编译器不会选择①处的声明,因为一个精确的匹配要优于显式转型所获得的匹配
return 0;
}
三、延迟实例化
编译器只对确实需要的部分实例化。换句话说,编译器会延迟模板的实例化。
(1)当隐式实例化类模板时,同时也实例化了该模板的每一个成员函数的声明,但并没有实例化相应的定义。
但是有些情况是不会延迟的,如下:
①类模板里面包含有匿名的union,那么,匿名的union成员同时也被实例化,
②虚函数,作为实例化类模板的结果,许多编译器实现都会实例化虚函数的定义,因为“实现虚函数调用机制的内部结构”要求虚函数的定义作为链接实体的存在。
(2)实例化类模板与实例化缺省的函数调用实参是分开的。换句话说,只有函数确实使用了缺省的实参,才会实例化该实参,如果这个函数不使用缺省的实参,那么就不会实例化该缺省的实参,而是显式使用实参来实例化。
template<typename T>
class Safe{};
template<int N>
class Danger{
typedef char Block[N]; //如果N<=0的话,将会出错
};
template<typename T, int N>
class Tricky{
public:
virtual ~Tricky(){} //虚函数,并提供了定义
void no_body_here(Safe<T>=3); //该缺省实参是可疑的,但没有被使用,不会被实例化,不会出错
void inclass(){
Danger<N>no_boom_yet; //没有被使用,不会被实例化,不会出错}
//void error(){ Danger<0> boom;} //如果没有被注释,会被要求给出这个类Danger<0>的完整定义,
//而实例化Danger<0>会出错,即使没有被使用,也不会被实例化,但仍然能够引发一个错误
//该错误是在泛模板处理中产生的
//void unsafe(T(*p)[N]); //如果 没有注释掉的话,此处实例化声明的时候会出错T operator->();
//virtual Safe<T> suspect(); //虚函数,但是没有提供定义,所以会引发一个链接期的错误,
//如果不注释掉的话,链接器就会给出这类错误
struct Nested{
Danger<N> pfew; //因为没有使用该结构,所以此处的没有实例化
};
union{
int align;
Safe<T> anonymous;
};
};
int main()
{
Tricky<int, 0> ok;
}
三、C++实例化模型
(1)两阶段查找
第一阶段:使用普通查找规则(在适当情况也会使用ADL)对模板进行解析,查找非依赖型名称。另外非受限的依赖型名称(诸如函数调用中的函数名称,因为其具有一个依赖型实参)也会在这个阶段查找,只不过查找不完全,在实例化模板的时候还会再次进行查找。
第二阶段:发生在模板被实例化的时候,我们也称此时发生的地点(或源代码的某个位置)为一个实例化点POI。依赖型受限名称就在此时查找。另外,非受限的依赖型名称在此阶段也会再次执行ADL查找.
(2)POI(实例化点)
①对于指向非类型(也就是函数╮( ̄▽ ̄")╭)特化的引用, C++把他的POI定义在“包含这个引用的定义或声明之后的最近名字空间域”。
class MyInt{
public:
MyInt(int i);
};
MyInt operator - (MyInt const);
bool operator >(MyInt const&, MyInt const&);
typedef MyInt int; //②
template <typename T>
void f(T i)
{
if(i>0){
g(-i); //①
}
}
//(1)
void g(Int) //这就是那个定义或声明
{
//(2)
f<Int>(42); //这就是那个引用
//(3)
}
//(4)这就是那个“之后最近的名字空间域”,函数f<int>的一个特化会出现在这里
【注意】①位置的名称g, 是非受限依赖型名称,因为他的参数是依赖型的, 所以会在第二阶段查找只是使用ADL就能够找到函数g(Int)其实也就是g(MyInt);如果将MyInt替换成int,即②处为typedef int Int,那么第二阶段的查找关联命名空间就会是空集,也就找不到函数g(Int)的声明和定义。
②对于产生自模板的类实例的引用,它的POI只能定义在“包含这个实例引用的定义或声明之前的最近名字空间域”。
template<typename T>
class S{
public:
T m;
};
//(5) 这里就是S的POI
unsigned long h() //这就是那个定义或声明
{
//(6)
return (unsigned long) sizeof(S<int>); //这就是那个实例引用
//(7)
}
//(8)
四、显式实例化
为模板特化显式的生成POI是可行的,我们把这种特化的构造成为显式实例化指示符。从语法关键字上讲,它有关键字template和后面的特化声明组成,所声明的特化就是即将有实例化获得的特化。
template<typename T>
void f(T) throw(T){}
下面有4个有效的显式实例化实体
template void f<int>(int) throw(int);
template void f<>(int) throw(int);
template void f(int) throw(int); //通过演绎获得
template void f(int); //异常规范也可以省略,如果没有省略,异常规范必须匹配相应的模板
C++规定: 同一个程序中,每一个特定的模板特化最多只能存在一处显式实例化。而且,如果摸个模板特化已经被显式实例化(使用template),那么就不能对其进行显式特殊化(使用template<>)。