当定义模板类时,除非程序中真正需要其定义,否则它是不会实例化的。例如:
template<typename T>
class Link {
Link *suc; //不需要Link的定义
//...
};
Link<int> *pl; //不需要Link的实例化
Link<int> lnk; //现在我们需要实例化Link了
对于一个模板函数,只有当它真正被使用时,才需要一个函数实现来实例化它。“被使用”的含义是“被调用或被获取地址”。特别是,实例化一个类模板并不意味着要实例化它的所有成员函数。考虑如下代码
template<typename T>
class List {
//...
void sort();
};
class Glob {
//...无比较运算符...
};
void f(List &lb, List<string> &ls)
{
ls.sort();
//...使用lb上的操作,但不包括lb.sort()...
}
其中List::sort()被实例化了,但List::sort()未被实例化。
显示实例化就是在一个特例化声明上加上关键字template前缀(template后面没有<)
template class vector<int>; //类
template int& vector<int>::operator[](int); //成员函数
template int convert<int, double>(double); //非成员函数
与模板函数调用类似,我们可以忽略从函数实参推断出的模板实参,例如
template int convert<int, double>(double); //正确(冗余的)
template int convert<int>(double); //正确
使用extern template来实现
#include "MyVector.h"
extern template class MyVector<int>; //禁止隐式实例化,在其他某处显式实例化
void foo(MyVector<int> &v)
{
//...在这里使用vector...
}
“其他某处”的代码可能像下面这样:
#include "MyVector"
template class MyVector<int>; //在此编译单元中实例化;使用此实例化点
bool tracing;
template<typename T>
T sum(std::vector &v)
{
T t{};
if (tracing)
cerr << "sum(" << &v << ")\n";
for(int i = 0; i != v.size(); i++)
t = t + v[i];
return t;
}
//...
#include
void f(std::vector &v)
{
Quard c = sum(v);
}
C++语言将模板定义中使用的名字分为以下两类
[1]依赖性名字;依赖于模板参数的名字,如上面的+,在实例化点完成绑定
[2]非依赖性名字:不依赖于模板参数的名字,如上面的vector、tracing,在定一点完成绑定
函数调用依赖一个模板参数当且仅当满足下列条件:
[1]根据类型推断规则,函数实参的类型依赖于一个模板参数T,例如f(T(1))、f(t)、f(g(t))及f(&t),假定t的类型是T。
[2]根据类型推断规则,函数有一个参数依赖于T,例如,f(T)、f(list&)及f(const T*)
template<typename T>
T f(T a)
{
return g(a); //正确:a是一个依赖性名字,因此g也是
}
class Quard {/*...*/};
void g(Quard);
int z = f(Quard{2}); //f的g绑定到g(Quard)
下面的名字不是依赖性的,会产生编译错误
class Quard{/*...*/};
template<typename T>
T ff(T a)
{
return gg(Quard{1}); //错误:作用域中没有gg(),gg(Quard{1}})并不依赖于T
}
int gg(Quard);
int zz = ff(Quard{2});
默认情况下,编译器假定依赖性名字不是类型名。因此,为了使依赖性名字可以是一个类型,你必须用关键字typename显式说明,如:
template<typename Container>
void fct(Container &c)
{
Container::value_type v1 = c[7]; //语法错误:编译器假定value_type不是类型名
typename Container::value_type v2 = c[9]; //正确:显式说明vlaue_type是类型
auto v3 = c[11]; //正确:让编译器推断
}
可以引用别名来避免使用typename的尴尬
template<typename Container>
using Value_type = typename T::value_type;
template<typename Container>
void fct2(Container &c)
{
Value_type v1 = c[7]; //正确
//...
}
类似地,命名.(点)、->或::后面的成员模板需要使用关键字template。例如:
class Pool {
public:
template<typename T> T* get();
template<typename T> void release(T*);
//...
};
template<typename Alloc>
void f(Alloc &all)
{
int *p1 = all.get<int>(); //语法错误:编译器get是非模板名
int *p2 = all.template get<int>(); //正确:编译器假定get()是一个模板
//...
}
void user(Pool &pool)
{
f(pool);
//
}
编译器将不依赖于模板实参的名字当作模板外的名字一样处理。因此,在定义点位置这种名字必须在作用域。
int x;
template<typename T>
T f(T a)
{
++x; //正确:x在作用域
++y; //错误:作用域中没有y,且y不依赖于T
return a; //正确:a依赖于T
}
int y;
int z = f(2);
如果找到了名字的声明,则编译器就会使用这个声明,即使随后可能发现“更好的”声明也是如此。
void g(double);
void g2(double);
template<typename T>
int ff(T a)
{
g2(2); //调用g2(double);
g3(2); //错误:作用域中没有g3()
g(2); //调用g(double);g(int)不在作用域
//...
}
void g(int);
void g3(int);
int x = ff(a);
依赖性名字含义所需的上下文由模板的使用决定,这被称作此特化版本的实例化点。
对于一个函数模板而言,此位置位于包含模板使用的最近的全局作用域或名字空间作用域中,恰好在包含此次使用的声明之后,例如:
void g(int);
template<typename T>
void f(T a)
{
g(a); //g在实例化点绑定
}
void h(int i)
{
extern void g(double);
f(i);
}
//f的实例化点
f的实例化点在h()之外,保证了h()中调用的g()是全局的g(int)而非局部的g(double)
下面的递归调用是合法的
void g(int);
template<typename T>
void f(T a)
{
g(a); //g在实例化点绑定
if (i) h(a - 1); //h在实例化点绑定
}
void h(int i)
{
extern void g(double);
f(i);
}
//f的声明点
对于一个模板类或一个类成员而言,实例化点恰好位于包含其使用的声明之前。
template<typename T>
class Container {
vector v; //元素
//...
public:
void sort(); //排序元素
//...
};
//Container的实例化点
void f()
{
Container<int> c; //使用点
c.sort();
}
在以下位置,编译器会为模板生成特例化版本
[1]任何实例化点
[2]任何编译单元的末尾
[3]或是为生成特例化而特别创建的编译单元
如果一个程序用相同的模板实参组合多次使用一个模板,则模板有多个实例化点。
如果选择不同的实例化点可能导致两种不同的含义,则程序是非法的,如下:
void f(int);
namespace N {
class X {};
char g(X, int);
}
template<typename T>
void ff(T t, double d)
{
f(d); //f绑定到f(int)
return g(t, d); //g可能绑定到g(X, int)
}
auto x1 = ff(N::X{}, 1.1); //ff;可能将g绑定到N::g(X, int),1.1窄化转换为1
Namespace N { //重新打开N声明double版本
double g(X, double);
}
auto x2 = ff(N::X, 2.2); //ff;将g绑定到N::g(X, double);最佳匹配
当函数被调用时,即使其声明不在当前作用域中,只要它是在某个实参所在的名字空间中声明的,编译器就能找到它。编译器完成依赖性名字的绑定是通过查看以下两条来实现的:
[1]模板定义点所处作用域中的名字
[2]依赖性调用的一个实参的名字空间中的名字
例如:
namespace N {
class A {/*...*/};
char f(A);
}
char f(int);
template<typename T>
char g(T t)
{
return f(t); //选择依赖于T的实参的f()
}
char f(double);
char c1 = g(N::A()); //导致N::f(N::A)
char c2 = g(2); //导致f(int)被调用
char c3 = g(2.1); //导致f(int)被调用;f(double)不会被考虑
注意,g(2.1)的调用f(int),是因为在模板声明定义时找不到下面的char f(double);
实参依赖查找(ADL)对避免冗长代码很有用
#include
int main()
{
std::cout << "Hello, world" << endl; //正确,因为使用了ADL
}
与未受限模板组合使用时,ADL可能显得“过于激进”了。如下
#include
#include
namespace User {
class Customer {/*...*/};
using Index = std::vector ;
void copy(const Index&, Index&, int deep);
void algo(Index& x, Index& y)
{
//...
copy(x, y, false); //错误
}
}
由于vector定义在std中,最终在中,找到了
template<typename In, typename Out>
Out copy(In, In, Out);
可通过显示指定来解决该问题,例如:
void User::algo(Index& x, Index& y)
{
User::copy(x, y, false); //正确
std::swap(*x[i], *x[j]); //正确:只会考虑std::swap
}
void g(int);
struct B {
void g(char);
void h(char);
};
template<typename T>
class X : public T {
public:
void f()
{
g(2); //调用g(int)
}
//...
};
void h(X x)
{
x.f();
}
g(2)调用了g(int),如果想让它调用T的可以有三种方法
[1]用依赖性类型限定名字,如T::g
[2]声明一个名字指向此类的一个对象,如this->g
[3]用using声明将名字引入作用域,如using T::g
例如:
void g(int);
void g2(int);
struct B {
using Type = int;
void g(char);
void g2(char);
};
template<typename T>
class X : public T {
public:
typename T::Type m; //正确
Type m2; //错误(Type不在作用域中)
using T::g2(); //将T::g2()引入作用域
void f()
{
this->g(2); //调用T::g
g(2); //调用::g(int)!!!
g2(2); //调用T::g2
}
//...
};
void h(X x)
{
x.f();
}