模板实例化
模板实例化是指从模板定义到生成实际代码的过程。
当需要区分编译器生成的特例化版本和程序员显式编写的特例化版本时,我们分别称它们为生成特例化(generated specialization)和显式特例化(explicit specialization)。显式特例化通常也被称为用户自定义特例化(user-defined specialization)或简称用户特例化(user specialization)。
何时需要实例化
模板的实例化发生在程序需要该模板类或函数的实际定义时。例如,当一个类的指针或引用被声明时并不需要实例化模板,而在使用该类的对象时才会进行实例化。
template<typename T>
class Link {
Link* suc; // 正确:还不需要Link的定义
// ...
};
Link<int>* a; // 还不需要实例化 Link<int>
Link<int> b; // 需要实例化 Link<string>
特别是,实例化一个模板类并不意味着要实例化它的所有成员函数。
手工控制实例化
C++ 语言不要求用户做任何事来完成模板实例化,但它确实提供了两种途径帮助用户在需要时控制实例化。需要控制实例化的原因包括:
- 通过消除冗余的重复实例化代码来优化编译和链接过程;
- 准确掌握哪些实例化点被使用,从而消除复杂名字绑定上下文带来的意外。
一个显式实例化请求(通常简称为显式实例化,explicit instantiation)在语法上就是一个特例化声明加上关键字 template
前缀(template
后面没有 <
)。
template class vector<int>; // 类
template int& vector<int>::operator[](int); // 成员函数
template int convert<double>(double); // 非成员函数
C++ 还提供了 extern template
关键字,允许在特定的编译单元中控制模板的实例化。此功能常用于外部模板声明,即在一个编译单元中声明模板,而在另一个编译单元中进行实例化,避免多次实例化。例如:
#include "MyVector.h"
extern template class MyVector<int>;
void foo(MyVector<int>& v) {
// 在这里使用 vector<int>
}
在其他文件中可以显式实例化 MyVector<int>
,从而在不同的编译单元中共享模板实例化结果。
名字绑定
在模板实例化时,C++ 编译器需要找到模板中使用的所有名字的定义,此过程称为名字绑定(Name Binding)。模板名字绑定的问题涉及多个上下文,包括:
- 模板定义的上下文
- 实参类型声明的上下文
- 模板使用的上下文
在模板编程中,名字查找分为依赖名字和非依赖名字:
- 依赖名字:依赖于模板参数的名字。这些名字的查找会延迟到实例化时进行。
- 非依赖名字:不依赖于模板参数的名字,在模板定义时即已完成绑定。
依赖性名字
称一个函数调用依赖一个模板参数当且仅当满足下列条件:
- 根据类型推断规则,函数的实参类型依赖于模板参数
T
。例如,f(T(1))
、f(t)
、f(g(t))
,假设t
的类型是T
。 - 函数有一个参数依赖于
T
,例如f(T)
、f(list<T>&)
等。
大体上,如果被调用函数的实参或形参明显依赖于模板参数,则函数名是依赖性名字。例如:
template<typename T>
T f(T a) {
return g(a); // 正确:a 是依赖性名称,因此g也是
}
class Quad {
public:
Quad(int) {}
};
Quad g(Quad a) {
return a;
}
auto z = f(Quad(2)); // f的g绑定到g(Quad)
如果一个函数调用碰巧与实际的模板参数类型匹配,则不是依赖性的。
class Quad {
public:
Quad(int) {}
};
template<typename T>
T ff(T a) {
return gg(Quad(2)); // 错误,作用域中没有gg(),gg(Quad(2))并不依赖T
}
Quad gg(Quad a) {
return a;
}
auto z = ff(Quad(2));
默认情况下,编译器假定依赖性名字不是类型名。因此,为了使依赖性名字可以是一个类型,你必须用关键字 typename
显式说明,例如:
template<typename Container>
void fct(Container& c)
{
Container::value_type v1 = c[7]; // 语法错误: 编译器假定 value_type 不是类型名
typename Container::value_type v2 = c[9]; // 正确: 显式说明 value_type 是类型
auto v3 = c[11]; // 正确: 让编译器推断
// ...
}
我们可以引人类型别名来避免使用 typename
的尴尬。例如:
template<typename T>
using Value_type = typename T::value_type;
template<typename Container>
void fct2(Container& c)
{
Value_type<Container> 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 在作用域内,且不依赖于 T
++y; // 错误:y 未在作用域内
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)
// ...
}
void g(int);
void g3(int);
int x = ff(a);
在这个示例中,ff()
内的 g(2)
调用了 g(double)
,而 g(int)
定义出现在模板定义点之后,因此编译器不会考虑它。
实例化点绑定
在 C++ 模板编程中,实例化点绑定是指确定依赖性名字(如函数或变量)所需的上下文。这个上下文由模板的使用(给定一组实参)决定。每次模板对一组给定模板实参的使用都定义了一个实例化点。
对于函数模板,实例化点位于包含模板使用的最近的全局作用域或名字空间作用域中,恰好在包含此次使用的声明之后(也是为了支持递归)。
void g(int);
template<typename T>
void f(T a)
{
g(a); // g 在实例化点绑定(将被绑定到全局的g,而不是局部的g)
if (i) h(a - 1); // h 在实例化点绑定
}
void h(int i)
{
extern void g(double);
f(i);
}
// f<int> 的声明点
对于模板类或类成员,实例化点位于包含其使用的声明之前(不然就找不到定义了)。
template<typename T>
class Container {
vector<T> v;
public:
void sort();
};
// Container<int> 的实例化点
void f() {
Container<int> c;
c.sort(); // 调用 Container<int>::sort()
}
多实例化点
在 C++ 模板编程中,编译器可以在多个位置为模板生成特例化版本,这些位置包括:
- 任何实例化点:如前所述,模板的每次具体使用都会创建一个实例化点。
- 任何编译单元的末尾:编译器可以在编译单元结束时生成所有特例化。
- 特别创建的编译单元中:编译器可以在处理完所有编译单元后,为整个程序生成所有特例化。
这些策略各有优缺点,可以组合使用。
如果一个程序用相同的模板实参组合多次使用一个模板,则模板有多个实例化点。如果选择不同的实例化点可能导致两种不同的含义,则程序是非法的:
void f(int);
namespace N {
class X {};
char g(X, int);
}
template<typename T>
int 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<N::X, double>;可能将g绑定到N::g(X,int), 1.1窄化转换为1
namespace N {
double g(X, double); // 重新打开N声明double版本
}
auto x2 = ff(N::X{}, 2.2); // ff<N::X, double>;将g绑定到N::g(X,double);最佳匹配
模板与命名空间
在调用模板函数时,即使某些函数名称未在当前作用域中声明,编译器依然可以在某个实参所属的命名空间中找到它。这种机制对于模板函数调用特别重要,因为实例化模板时需要找到依赖性函数。
编译器在模板实例化时可以通过以下两条途径来查找依赖性函数:
- 模板定义处所在作用域中的名称。
- 依赖性实参的一个实参命名空间中的名称。
namespace N {
class A { /* ... */ };
char f(A); // 命名空间N内的f(A)
}
char f(int); // 全局作用域的f(int)
template<typename T>
char g(T t) {
return f(t); // f依赖于T的实参类型
}
char f(double); // 全局作用域的f(double)
char c1 = g(N::A()); // 调用 N::f(N::A)
char c2 = g(2); // 调用全局作用域的 f(int)
char c3 = g(2.1); // 错误,f(double) 不会被考虑
来自基类的名字
当一个类模板继承一个基类时,可能是:
-
基类依赖于一个模板参数
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<B> x) { x.f(); }
-
基类不依赖于模板参数
void g(int); struct B { void g(char); void h(char); }; template<typename T> class X : public B { public: void f() { g(2); // 调用 B::g(char) h(2); // 调用 X::h(int) } void h(int); };
模板实例化是C++中从模板定义生成实际代码的过程。编译器在需要使用模板类或函数的实际定义时进行实例化。实例化一个模板类并不意味着要实例化其所有成员函数。C++提供了显式实例化和`extern template`关键字来帮助用户控制模板的实例化,优化编译和链接过程,并消除名字绑定带来的意外。
名字绑定是指在模板实例化时,编译器找到模板中使用的所有名字的定义。依赖名字的查找会延迟到实例化时进行,而非依赖名字在模板定义时即已完成绑定。默认情况下,编译器假定依赖名字不是类型名,除非用`typename`显式说明。类似地,引用成员模板时需使用`template`关键字。
通过理解模板实例化和名字绑定的机制,程序员可以更有效地控制和优化模板代码的生成和使用。