第26章 实例化(Instantiation)
目录
26.2 模板实例化(Template Instantiation)
26.2.1 何时需实例化?(When Is Instantiation Needed?)
26.2.2 手动控制实例化(Manual Control of Instantiation)
26.3 名称绑定(即定位名称的声明)(Name Binding)
26.3.2 定义绑定点(Point-of-Definition Binding)
26.3.3 实例化点的绑定(Point-of-Instantiation Binding)
26.3.4 多实例化点(Multiple Instantiation Points)
26.3.5 模板和名称空间(Templates and Namespaces)
26.3.6 激进的参数依赖查找(Overaggressive ADL)
26.3.7 来自基类的名称(Names from Base Classes)
26.1 引言(Introduction)
模板的一大优势在于,它是一种非常灵活的代码组合机制。为了产生出色的代码质量,编译器会将来自以下来源的代码(信息)组合起来:
• 模板定义及其词法环境,
• 模板参数及其词法环境,以及
• 模板的使用环境。
实现这一性能的关键在于编译器能够同时查看来自这些上下文的代码,并利用所有可用信息将其编织在一起。这样做的问题是模板定义中的代码并不像我们希望的那样局部化(在其他条件相同的情况下)。有时,我们可能会对模板定义中使用的名称的含义感到困惑:
• 它是局部名称吗?
• 它是与模板参数关联的名称吗?
• 它是来自层次结构中基类的名称吗?
• 它是来自命名空间的名称吗?
• 它是全局名称吗?
本章讨论与名称绑定相关的问题,并考虑它们对编程风格的影响。
• §3.4.1 和 §3.4.2 介绍了模板。
• 第 23 章 详细介绍了模板和模板参数的使用。
• 第 24 章 介绍了泛型编程和概念的关键概念。
• 第 25 章 详细介绍了类模板和函数模板,并介绍了特化的概念。
• 第 27 章 讨论了模板和类层次结构之间的关系(支持泛型和面向对象编程)。
• 第 28 章 重点介绍模板作为生成类和函数的语言。
• 第 29 章 提供了一个更大的示例,说明如何结合使用语言功能和编程技术。
26.2 模板实例化(Template Instantiation)
给定一个模板定义和该模板的使用,实现的工作就是生成正确的代码。从类模板和一组模板参数,编译器需要生成类的定义以及程序中使用的成员函数的定义(并且仅限于这些成员函数;§26.2.1)。基于模板函数和一组模板参数生成一个函数。这个过程通常称为模板实例化。
(基于模板和一组模板参数)生成类和函数称为特化。当我们需要区分生成的特化和程序员明确编写的特化(§25.3)时,我们分别指生成的特化(generated specializations)和显式特化(explicit specializations)。显式特化通常称为用户定义的特化,或简称为用户特化。
要在非平凡程序中使用模板,程序员必须了解模板定义中使用的名称如何绑定到声明以及如何组织源代码的基本知识(§23.7)。
在默认情况下,编译器根据名称绑定规则 (§26.3) 从使用的模板生成类和函数。也就是说,程序员不需要明确说明必须生成哪些版本的模板。这很重要,因为程序员很难确切知道需要哪些版本的模板。通常,程序员甚至没有听说过的模板也用于库的实现,有时程序员知道的模板与未知的模板参数类型一起使用。例如,标准库map (§4.4.3,§31.4.3) 是用红黑树模板实现的,其中的数据类型和操作只有最好奇的用户才知道。通常,只有通过递归检查应用程序代码库中使用的模板才能知道所需的生成函数集。计算机比人类更适合进行此类分析。
在另一方面,对于程序员来说,能够具体说明代码应从模板生成的位置(§26.2.2)有时很重要。通过这样做,程序员可以对实例化的上下文进行详细的控制。
26.2.1 何时需实例化?(When Is Instantiation Needed?)
仅当需要类的定义时,才需要生成类模板的特化(§iso.14.7.1)。特别是,要声明指向某个类的指针,不需要类的实际定义。例如:
class X;
X∗ p; // OK: 无需 X 的定义
X a; // 错: 需 X 定义
在定义模板类时,这种区别至关重要。除非实际需要定义模板类,否则不会实例化模板类。例如:
template<typename T>
class Link {
Link∗ suc; // OK: (暂时)无需定义Link
// ...
};
Link<int>∗ pl; // 暂时不需要实例化 Link<int>
Link<int> lnk; // 现在我们需要实例化 Link<int>
使用模板的地方定义了一个实例点(§26.3.3)。
仅当模板函数已使用时,实现才会实例化该函数。所谓“使用”,是指“调用或获取其地址”。具体而言,类模板的实例化并不意味着其所有成员函数的实例化。这为程序员在定义模板类时提供了重要的灵活性。请考虑:
template<typename T>
class List {
// ...
void sort();
};
class Glob {
// ... 无比较运算符 ...
};
void f(List<Glob>& lb, List<string>& ls)
{
ls.sort();
// ... 使用基于lb的运算, 而不是 lb.sort() ...
}
这里,List<string>::sort() 被实例化,但 List<Glob>::sort() 没有实例化。这既减少了生成的代码量,也使我们不必重新设计程序。如果 List<Glob>::sort() 已经生成,我们要么必须将 List::sort() 所需的操作添加到 Glob,要么重新定义 sort() 以使其不是 List 的成员(无论如何,这是更好的设计),要么使用其他容器来容纳 Glob。
26.2.2 手动控制实例化(Manual Control of Instantiation)
该语言不需要任何显式的用户操作来实现模板实例化。但是,它确实提供了两种机制来帮助用户在需要时进行控制。有时,这种需求是出于希望
• 通过消除冗余的重复实例来优化编译和链接过程,或者
• 准确了解哪个实例点用于消除复杂名称绑定(name-binding)上下文带来的意外。
显式实例化请求(通常简称为显式实例化)是以关键字 template 为前缀(无需后接 < )的特化声明:
template class vector<int>; // 类
template int& vector<int>::operator[](int); // 成员函数
template int convert<int,double>(double); // 非成员函数
模板声明以 template< 开头,而普通模板则以实例化请求开头。请注意,模板以完整声明作为前缀;仅表述名称是不够的:
template vector<int>::operator[]; // 语法错误
template convert<int,double>; // 语法错误
与模板函数调用一样,可以从函数参数推导出的模板参数可以省略(§23.5.1)。例如:
template int convert<int,double>(double); // OK (多余)
template int convert<int>(double); //OK
当类模板显式实例化时,其每个成员函数也会实例化。
实例化请求对链接时间和重新编译效率的影响可能很大。我见过一些例子,将大多数模板实例捆绑到单个编译单元中可以将编译时间从几个小时缩短到几分钟。
同一特化有两个定义是错误的。无论这种多重特化是用户定义的(§25.3)、隐式生成的(§23.2.2)还是显式请求的,都没有关系。但是,编译器不需要在单独的编译单元中诊断多个实例。这允许智能实现忽略冗余实例,从而避免与使用显式实例从库中组合程序相关的问题。但是,实现不需要是智能的。“不太智能”的用户实现必须避免多重实例。如果他们不这样做,最糟糕的情况是他们的程序将无法链接;不会有任何默默的含义变化。
为了与显式实例化请求互补,该语言提供了非实例化的显式请求(通常称为extern template)(译注:即不在此处实例化,在别处实例化)。典型用途是为特化进行一次显式实例化,并使用extern template在其他编译单元中使用。这反映了一个定义和多个声明的经典用法(§15.2.3)。例如:
#include "MyVector.h"
extern template class MyVector<int>; // 抑制隐式实例化
// 别处显式实例化
void foo(MyVector<int>& v)
{
// ... use the vector in here ...
}
“别处”实例化可能看起来像这样:
#include "MyVector.h"
template class MyVector<int>; // 在这个编译单元实例化;使用这个实例点
除了为类的所有成员生成特化之外,显式实例化还确定单个实例化点,以便可以忽略其他实例化点(§26.3.3)。显式实例化的一个用途是将其放置在共享库中。
26.3 名称绑定(即定位名称的声明)(Name Binding)
定义模板函数以最小化对非局部信息的依赖。原因是模板将用于基于未知类型和未知上下文生成函数和类。每个细微的上下文依赖关系都可能对某些人造成问题 ——并且某些人不太可能想知道模板的实现细节。在模板代码中,应特别认真地考虑尽可能地避免使用全局名称的一般规则。因此,我们尝试使模板定义尽可能独立,并以模板参数的形式提供大部分原本是全局上下文的内容(例如,特征(traits);§28.2.4,§33.1.3)。使用概念来记录对模板参数的依赖关系(§24.3)。
但是,为了实现模板的最优雅的表述,必须使用一些非局部名称的情况并不罕见。特别是,编写一组协作的模板函数比编写一个自包含函数更常见。有时,这样的函数可以是类成员,但并非总是如此。有时,非本地函数是最佳选择。典型的例子是 sort() 对 swap() 和 less() 的调用(§25.3.4)。标准库算法是一个大规模的例子(第 32 章)。当某些东西需要是非局部的时候,最好使用命名的名称空间而非全局作用域。这样做可以保留一些局部性。
具有常规名称和语义的运算(例如 +、∗、[] 和 sort())是模板定义中非局部名称使用的另一个来源。考虑:
bool tracing;
template<typename T>
T sum(std::vector<T>& v)
{
T t {};
if (tracing)
cerr << "sum(" << &v << ")\n";
for (int i = 0; i!=v.siz e(); i++)
t = t + v[i];
return t;
}
// ...
#include<quad.h>
void f(std::vector<Quad>& v)
{
Quad c = sum(v);
}
看似无害的模板函数 sum() 依赖于其定义中未明确指定的几个名称,例如tracing、cerr 和 + 运算符。在此示例中,+ 在 <quad.h> 中定义:
Quad operator+(Quad,Quad);
重要的是,在定义 sum() 时,与 Quad 相关的任何内容都不在作用域内,并且不能假设 sum() 的编写者知道 Quad 类。特别是,在程序文本中,+ 的定义可能晚于 sum()(译注:指定义的位置),甚至在时间上更晚(译注:应按编译时间)。
查找模板中显式或隐式使用的每一个名称的声明的这个过程称为名称绑定。模板名称绑定的一般问题是模板实例化涉及三个上下文,并且它们无法完全分离:
[1] 模板定义的上下文
[2] 参数类型声明的上下文
[3] 模板使用的上下文
在定义函数模板时,我们希望确保有足够的上下文,以便模板定义能够理解其实际参数,而不会从使用点的环境中拾取“意外的东西”。为了实现这一点,语言将模板定义中使用的名称分为两类:
[1] 依赖名称:依赖于模板参数的名称。此类名称在实例化点处绑定(§26.3.3)。在 sum() 示例中,+ 的定义可以在实例化上下文中找到,因为它采用模板参数类型的操作数。
[2] 非依赖名称:不依赖于模板参数的名称。此类名称在模板定义点处绑定(§26.3.2)。在 sum() 示例中,模板向量在头文件 <vector> 中定义,当编译器遇到 sum() 的定义时,布尔tracing 处于作用域内。
要考虑,依赖名称和独立名称都必须在其使用点的作用域内,或者可以通过参数相关查找找到(ADL;§14.2.4)。
以下小节详细介绍了模板定义中的依赖和非依赖名称如何绑定到特化。有关完整详细信息,请参阅 §iso.14.6。
26.3.1 依赖名称(Dependent Names)
“N 依赖于模板参数 T” 的最简单定义是“N 是 T 的成员”。遗憾的是,这还不够;Quads 的添加(§26.3)是一个反例。因此,当且仅当以下条件之一成立时,函数调用才称为依赖于模板参数:
[1] 根据类型推导规则(§23.5.2),实参的类型依赖于模板参数 T,例如,f(T(1))、f(t)、f(g(t)) 和 f(&t)(假设 t 是一个 T类型)。
[2] 根据类型推导规则(§23.5.2),被调用的函数具有依赖于 T 的参数,例如,f(T)、f(list<T>&) 和 f(const T∗)。
基本上,如果通过查看被调用函数的参数或形式参数可以明显看出其依赖性,则该函数的名称就是依赖的。例如:
template<typename T>
T f(T a)
{
return g(a); // OK: a 是一个依赖名称,因此后面的 g 也是
}
class Quad { /* ... */ };
void g(Quad);
int z = f(Quad{2}); // f的g 绑定到g(Quad)
如果调用恰巧有一个与实际模板参数类型匹配的参数,则该调用不是依赖的。例如:
class Quad { /* ... */ };
template<typename T>
T ff(T a)
{
return gg(Quad{1}); // 错: 在作用域内无gg() 而gg(Quad{1}) 不依赖T
}
int gg(Quad);
int zz = ff(Quad{2});
如果 gg(Quad{1}) 被视为依赖,那么它的含义对于模板定义的读者来说将是极其神秘的。如果程序员想要调用 gg(Quad),则应将 gg(Quad) 的声明放在 ff() 的定义之前,以便在分析 ff() 时 gg(Quad) 处于作用域内。这与非模板函数定义的规则完全相同(§26.3.2)。
默认情况下,依赖名称被假定为命名非类型的东西。因此,要将依赖名称用作类型,你必须使用关键字 typename 来说明。例如:
template<typename Container>
void fct(Container& c)
{
Container::value_type v1 = c[7]; // 语法错误: value_type 假设为非类型名
typename Container::value_type v2 = c[9]; // OK: value_type 假设为一个类型
auto v3 = c[11]; // OK: 让编译器解决
// ...
}
我们可以通过引入类型别名(§23.6)来避免这种尴尬的类型名称的使用。例如:
template<typename T>
using Value_type<T> = typename T::value_type;
template<typename Container>
void fct2(Container& c)
{
Value_type<Container> v1 = c[7]; // OK
// ...
}
在 .(点)、−> 或 :: 后命名成员模板需要类似使用关键字模板。例如:
class Pool { // some allocator
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>(); // OK: get()假设为命名一个模板
// ...
}
void user(Pool& pool){
{
f(pool);
// ...
}
与使用 typename 明确说明假定名称用于命名类型相比,使用 template 明确说明假定名称用于命名模板的情况很少见。请注意消除歧义关键字位置的不同:typename 出现在限定名称之前,而 template 出现在模板名称之前。
26.3.2 定义绑定点(Point-of-Definition Binding)
当编译器看到模板定义时,它会确定哪些名称是依赖的(§26.3.1)。如果名称是依赖的,则查找其声明将推迟到实例化时(§26.3.3)。
不依赖于模板参数的名称将视为不在模板中的名称;它们必须在定义时处于作用域内(§6.3.4)。例如:
int x;
template<typename T>
T f(T a)
{
++x; // OK: x 在作用域内
++y; // 错: 作用域内无y, 且 y 不依赖 T
return a; // OK: a 是一个依赖
}
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);
这里,ff() 将调用 g(double)。g(int) 的定义来得太晚,无法考虑——就好像 ff() 不是模板,或者 g 命名了一个变量。
26.3.3 实例化点的绑定(Point-of-Instantiation Binding)
用于确定依赖名称含义的上下文(§26.3.1)由具有给定参数集的模板的使用决定。这称为该特化的实例化点(§iso.14.6.4.1)。对于给定的模板参数集,每次使用模板都会定义一个实例化点。对于函数模板,该点位于最近的全局或命名空间作用域内,包围其使用,紧接着包含该使用的声明。例如:
void g(int);
template<typename T>
void f(T a)
{
g(a); // g 绑定一个实例化点
}
void h(int i)
{
extern void g(double);
f(i);
}
// f<int>的声明点
f<int>() 的实例化点在 h() 之外。这对于确保 f() 中调用的 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<int>声明点
这里,在 h() 定义之后具有实例化点是允许(间接递归)调用 h(a−1) 所必需的。
对于模板类或类成员,实例化点就在包含其使用的声明之前。
template<typename T>
class Container {
vector<T> v; // 元素
// ...
public:
void sort(); // 排序元素
// ...
};
// Container<int>的实例化点
void f()
{
Container<int> c; // 使用点
c.sort();
}
如果实例化点位于 f() 之后,则调用 c.sort() 将无法找到 Container<int> 的定义。
依靠模板参数来明确依赖关系可以简化我们对模板代码的思考,甚至允许我们访问局部信息。例如:
void fff()
{
struct S { int a,b; };
vector<S> vs;
// ...
}
这里,S 是一个局部名称,但由于我们将其用作显式参数,而不是试图将其名称埋在vector的定义中,因此我们没有潜在的令人惊讶的微妙之处。
那么,为什么我们不在模板定义中完全避免使用非局部名称呢?这当然可以解决名称查找的技术问题,但是——对于普通函数和类定义——我们希望能够在代码中自由使用“其他函数和类型”。将每个依赖项变成参数可能会导致非常混乱的代码。例如:
template<typename T>
void print_sorted(vector<T>& v)
{
sort(v.begin(),v.end());
for (const auto T& x : v)
cout << x << '\n';
}
void use(vector<string>& vec)
{
// ...
print_sorted(vec); // 使用std::sort排序, 然后使用std::cout打印
}
这里我们只使用两个非局部名称(sort 和 cout,均来自标准库)。要消除这些名称,我们需要添加参数:
template<typename T, typename S>
void print_sorted(vector<T>& v, S sort, ostream& os)
{
sort(v.begin(),v.end());
for (const auto T& x : v)
os << x << '\n';
}
void fct(vector<string>& vec)
{
// ...
using Iter = decltype(vs.begin()); // vec的iterator类型
print_sorted(some_vec,std::sort<Iter>,std::cout);
}
在这个简单的例子中,有很多理由可以消除对全局名称 cout 的依赖。但是,一般来说,如 sort() 所示,添加参数会使代码变得更加冗长而不一定使其更容易理解。
此外,如果模板的名称绑定规则比非模板代码的规则严格得多,那么编写模板代码就与编写非模板代码完全不同。模板和非模板代码将不再简单自由地进行互操作。
26.3.4 多实例化点(Multiple Instantiation Points)
可以生成模板特化
• 在任何实例化点(§26.3.3),
• 在编译单元中此后的任何点,
• 或在专门为生成特化而创建的编译单元中。
这反映了实现可用于生成特化的三种明显的策略:
[1] 第一次看到调用时生成特化。
[2] 在编译单元结束时,生成所需的所有特化。
[3] 一旦看到程序的每个编译单元,就生成程序所需的所有特化。
这三种策略都有优点和缺点,并且这些策略的组合也是可能的。
因此,多次使用同一组模板参数的模板具有多个实例化点。如果可以通过选择不同的实例化点来构造两个不同的含义,则程序是无效的。也就是说,如果依赖或非依赖名称的绑定可以不同,则程序是无效的。例如:
void f(int); // 在此, 我采用int
namespace N {
class X { };
char g(X,int);
}
template<typename T>
void ff(T t, double d)
{
f(d); //f is bound to 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 { // 重开N 以处理double
double g(X,double);
}
auto x2 = ff(N::X,2.2); // ff<N::X,double>; 绑定g 至 N::g(X,double); 最佳匹配
在两个声明之间调用重载函数是一种草率的编程。然而,如果查看大型程序,程序员没有理由怀疑存在问题。在这种特殊情况下,编译器可以捕获歧义。然而,类似的问题可能发生在单独的编译单元中,然后检测就会变得更加困难(对于编译器和程序员而言)。实现没有义务捕获此类问题。
为了避免令人惊讶的名称绑定,请尝试限制模板中的上下文依赖性。
26.3.5 模板和名称空间(Templates and Namespaces)
当调用函数时,即使函数不在作用域内,只要它在与其参数之一相同的命名空间中声明(§14.2.4),就可以找到其声明。这对于在模板定义中调用的函数很重要,因为它是在实例化期间找到依赖函数的机制。依赖名称的绑定是通过查看来完成的(§iso.14.6.4.2)。
[1] 模板定义点范围内的名称,加上
[2] 依赖调用的参数命名空间中的名称(§14.2.4)。
例如:
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) (译注:实测调用f(double))
这里,f(t) 显然是依赖的,所以我们不能在定义点绑定 f。为了为 g<N::A>(N::A) 生成特化,实现在命名空间 N 中查找名为 f() 的函数并找到 N::f(N::A)。
找到 f(int) 是因为它在模板定义点的作用域内。找不到 f(double) 是因为它不在模板定义点的作用域内(§iso.14.6.4.1),并且参数相关查找(§14.2.4)找不到只接受内置类型参数的全局函数。我发现很容易忘记这一点。
26.3.6 激进的参数依赖查找(Overaggressive ADL)
参数依赖查找(通常称为 ADL)对于避免冗长非常有用(§14.2.4)。例如:
#include <iostream>
int main()
{
std::cout << "Hello, world" << endl;// OK 由于 ADL
}
如果没有依赖参数的查找,endl 操作符将无法找到。事实上,编译器注意到 << 的第一个参数是在 std 中定义的 ostream。因此,它在 std 中查找 endl 并找到它(在 <iostream> 中)。
然而,当与不受约束的模板结合使用时,ADL 可能会“过于激进”。考虑一下:
#include<vector>
#include<algorithm>
// ...
namespace User {
class Customer { /* ... */ };
using Index = std::vector<Customer∗>;
void copy(const Index&, Index&, int deep); // 深复制或浅复制取决于 deep 的值
void algo(Index& x, Index& y)
{
// ...
copy(x,y,false); // error
}
}
可以猜测 User 的作者希望 User::alg() 调用 User::copy()。然而,事实并非如此。编译器注意到 Index 实际上是在 std 中定义的vector,并查看 std 中是否有相关函数可用。在 <algorithm> 中,它发现:
template<typename In, typename Out>
Out copy(In,In,Out);
显然,这个通用模板与 copy(x,y,false) 完美匹配。另一方面,User 中的 copy() 只能通过 bool-to-int 转换来调用。对于此示例,与等效示例一样,编译器的解析对大多数程序员来说都是一个意外,并且是一个非常隐蔽的错误的来源。使用 ADL 查找完全通用的模板可以说是语言设计错误。毕竟,std::copy() 需要一对迭代器(而不仅仅是两个相同类型的参数,例如两个 Index)。标准是这么说的,但代码却不是。许多这样的问题可以通过使用概念(§24.3,§24.3.2)来解决。例如,如果编译器知道 std::copy() 需要两个迭代器,就会产生一个非隐蔽的错误。
template<typename In, typename Out>
Out copy(In p1, In p2, Out q)
{
static_assert(Input_iterator<In>(), "copy(): In is not an input iterator");
static_assert(Output_iterator<Out>(), "copy(): Out is not an output iterator");
static_assert(Assignable<Value_type<Out>,Value_type<In>>(), "copy(): value type mismatch");
// ...
}
更好的是,编译器会注意到 std::copy() 甚至不是该调用的有效候选,因此会调用 User::copy()。例如(§28.4):
template<typename In, typename Out,
typename = enable_if(Input_iterator<In>()
&& Output_iterator<Out>()
&& Assignable<Value_type<Out>,Value_type<In>>())>
Out copy(In p1, In p2, Out q)
{
// ...
}
遗憾的是,许多这样的模板都位于用户无法修改的库中(例如,标准库)。
避免在包含类型定义的头文件中使用完全通用(完全不受约束)的函数模板是个好主意,但这很难避免。如果您需要一个,使用约束检查来保护它通常是值得的。
如果库中有导致问题的不受约束的模板,用户该怎么办?通常,我们知道函数应该来自哪个命名空间,因此我们可以具体说明这一点。例如:
void User::algo(Index& x, Index& y)
{
User::copy(x,y,false); // OK
// ...
std::swap(∗x[i],∗x[j]); // OK: 仅考虑 std::swap
}
如果我们不想具体说明要使用哪个命名空间,但想确保函数的特定版本通过函数重载得到考虑,我们可以使用 using 声明(§14.2.2)。例如:
template<typename Range, typename Op>
void apply(const Range& r, Op f)
{
using std::begin;
using std::end;
for (auto& x : r)
f(x);
}
现在,标准的 begin() 和 end() 位于 range-for 用于遍历 Range 的重载集中(除非 Range 具有成员 begin() 和 end();§9.5.1)。
26.3.7 来自基类的名称(Names from Base Classes)
当类模板具有基类时,它可以访问该基类中的名称。至于其他名称,有两种不同的可能性:
• 基类依赖于模板参数。
• 基类不依赖于模板参数。
后一种情况比较简单,在非模板类中,它就像基类一样处理。例如:
void g(int);
struct B {
void g(char);
void h(char);
};
template<typename T>
class X : public B {
public:
void h(int);
void f()
{
g(2); // call B::g(char)
h(2); // call X::h(int)
}
// ...
};
和以往一样,局部名称会隐藏其他名称,因此 h(2) 绑定到 X::h(int),而 B::h(char) 则不会被考虑。类似地,调用 g(2) 绑定到 B::g(char),而不考虑在 X 之外声明的函数。也就是说,全局 g() 不会被考虑。
对于依赖于模板参数的基类,我们必须更加小心,明确我们想要什么。考虑一下:
void g(int);
struct B {
void g(char);
void h(char);
};
template<typename T>
class X : public T {
public:
void f()
{
g(2); // call ::g(int)
}
// ...
};
void h(X<B> x)
{
x.f();
}
为什么 g(2) 不调用 B::g(char)(如上例所示)?因为 g(2) 不依赖于模板参数 T。因此它在定义时就被绑定了;模板参数 T(恰好用作基类)中的名称尚不清楚,因此不予考虑。如果我们想要考虑依赖类中的名称,就必须明确依赖关系。我们有三种方法可以做到这一点:
• 使用依赖类型限定名称(例如,T::g)。
• 声明名称引用此类的对象(例如,this−>g)。
• 使用 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; // OK
Type m2; // 错(作用域内无 Type)
using T::g2(); // 将T::g2()带入作用域
void f()
{
this−>g(2); // call T::g
g(2); //call ::g(int); 差异?
g2(2); //call T::g2
}
// ...
};
void h(X<B> x)
{
x.f();
}
只有在实例化时,我们才能知道用于参数 T(这里是 B)的参数是否具有所需的名称。
很容易忘记从基类中限定名称,而限定的代码通常看起来有点冗长和混乱。然而,另一种方法是,模板类中的名称有时会绑定到基类成员,有时会绑定到全局实体,具体取决于模板参数。这也不是理想的,语言规则支持经验法则,即模板定义应该尽可能自包含(§26.3)。
对模板依赖基成员的访问权限进行限定可能会很麻烦。但是,明确的限定可以帮助维护者,因此初始作者不应该对额外的输入抱怨太多。当整个类层次结构被模板化时,通常会发生此问题。例如:
template<typename T>
class Matrix_base { // 矩阵的内存, 所有元素的运算
// ...
int size() const { return sz; }
protected:
int sz; // 元素数量
T∗ elem; // 矩阵元素
};
template<typename T, int N>
class Matrix : public Matrix_base<T> { // N维矩阵
// ...
T∗ data() // 返向指向存储元素的指针
{
return this−>elem;
}
};
在此,要求 this−> 限定符。
26.4 建议(Advice)
[1] 让编译器/实现根据需要生成特化;§26.2.1。
[2] 如果需要精确控制实例化环境,则显式实例化;§26.2.2。
[3] 如果优化生成特化所需的时间,则显式实例化;§26.2.2。
[4] 避免模板定义中的细微上下文依赖关系;§26.3。
[5] 在模板定义中使用时,名称必须在作用域内,或者可以通过参数相关查找(ADL)找到;§26.3,§26.3.5。
[6] 保持实例化点之间的绑定上下文不变;§26.3.4。
[7] 避免使用可以通过 ADL 找到的完全通用模板;§26.3.6。
[8] 使用概念和/或 static_assert 避免使用不适当的模板;§26.3.6。
[9] 使用 using 声明来限制 ADL 的范围;§26.3.6。
[10] 根据需要使用 −> 或 T:: 限定模板基类中的名称;§26.3.7。
内容来源:
<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup