目录
8 深入模板基础
8.1 参数化声明
8.1.1 虚成员函数
8.1.2 模板的链接
8.2 模板参数
8.2.1 类型参数
8.2.2 非类型参数
8.2.3 模板的模板参数
8.2.4 缺省模板实参
8.3 模板实参
8.3.1 函数模板实参
8.3.2 类型实参
8.3.3 非类型实参
8.3.4 模板的模板实参
8.3.5 实参的等价性
8.4 友元
8.4.1 友元函数
8.4.2 友元模板
在这一章里, 我们将深入回顾之前所提到的一些基础知识: 模板的声明、 模板参数的约束以及模板实参的约束等。
C++现今支持两种基本类型的模板: 类模板和函数模板,这个分类实际上还包含成员模板。这些模板的声明和普通类与普通函数的声明很相似, 唯一的区别就是模板声明需要引入一个参数化子句:
template<...parameters here...>
联合(UNION)模板也是允许的(往往被看作类模板的一种):
template
union AllocChunk
{
T object;
unsigned char bytes[sizeof(T)];
};
和普通函数声明一样, 函数模板声明也可以具有缺省调用实参:
template
void report_top(Stack const&, int number = 10);
template
void fill(Array*, T const& = T());//对于基本类型,T()为0
后一个声明说明了: 缺省调用实参可以依赖于模板参数。 显然, 当fill()函数被调用时, 如果提供了第2个函数调用参数的话, 就不会实例化这个缺省实参。 这同时说明了: 即使不能基于特定类型T来实例化缺省调用实参, 也可能不会出现错误。 例如:
class Value {
public:
Value(int); //不存在缺省构造函数
};
void init(Array* array)
{
Value zero(0);
fill(array, zero); //正确: 没有使用 =T()
fill(array); //错误: 使用了=T(), 但当T =Value时缺省构造函数无效
}
除了两种基本类型的模板之外, 还可以使用相似的符号来参数化其他的3种声明。 这3种声明分别都有与之对应的类模板成员的定义:
(1) 类模板的成员函数的定义。
(2) 类模板的嵌套类成员的定义。
(3) 类模板的静态数据成员的定义。
成员函数模板不能被声明为虚函数。 这是一种需要强制执行的限制。因为虚函数调用机制的普遍实现都使用了一个大小固定的表, 每个虚函数都对应表的一个入口。然而, 成员函数模板的实例化个数, 要等到整个程序都翻译完毕才能够确定, 这就和表的大小(是固定的) 发生了冲突。相反, 类模板的普通[6]成员可以是虚函数, 因为当类被实例化之后, 它们的个数是固定的:
template
class Dynamic
{
public:
virtual ~Dynamic(); //OK: 每个Dynamic只对应一个析构函数
template
virtual void copy(T2 const&);
//错误: 在确定Dynamic实例的时候, 并不知道copy()的个数
};
每个模板都必须有一个名字, 而且在它所属的作用域下, 该名字必须是唯一的; 除非函数模板可以被重载 。 特别是, 类模板不能和另外一个实体共享一个名称, 这一点和class类型是不同的:
int C;
class C;//正确: 类名称和非类名称位于不同的名字空间
int X;
template
class X; //错误: 和变量X冲突
struct S;
template
class S; //错误, 和struct S冲突
模板名字是具有链接的, 但它们不能具有C链接。 但我们在大多数情况下所说的是标准的链接, 同时也存在非标准的链接, 它们可以具有一个依赖于实现的含义(然而, 我们还没发现有用于支持非标准模板名字链接的编译器实现):
extern"C++"template
void normal(); //这是缺省情况, 上面的链接规范可以不写
extern"C"template
void invalid(); //错误的: 模板不能具有C链接
extern"Xroma"template
void xroma_link(); //非标准的, 但某些编译器将来可能支持与Xroma语言的链接兼容性
模板通常具有外部链接。 唯一的例外就是前面有 static 修饰符的名字空间作用域下的函数模板:
template
void external(); //作为一个声明, 引用位于其他文件的、 具有
//相同名称的实体; 即引用位于其他文件的external()函数模板, 也称前置声明
template
static void internal(); //与其他文件中具有相同名称的模板没有关系即不是外部链接
因此我们知道(由于外部链接) : 不能在函数内部声明模板。
现今存在3种模板参数:
(1) 类型参数(它们是使用得最多的) 。
(2) 非类型参数。
(3) 模板的模板参数。
从前面知道, 模板声明要引入参数化子句, 模板参数就是在该子句中声明的。 这类声明可以把模板参数的名称省略不写(就是说, 在后面不会引用该名称的前提下) :
template //省略不写。
class X;
显然, 如果在模板声明后面需要引用参数名称, 那么这些参数名称是一定要写上的。 另外, 在同一对尖括号内部, 位于后面的模板参数声明可以引用前面的模板参数名称(但前面的不能引用后面的) :
template class Buf>//声明中都使用了第1个参数T
class Structure;
类型参数是通过关键字typename或者class引入的: 它们两者几乎是等同的。 关键字后面必须是一个简单的标识符, 后面用逗号来隔开下一个参数声明, 等号(=) 代表接下来的是缺省模板实参, 一个封闭的尖括号(>) 表示参数化子句的结束。
在模板声明内部, 类型参数的作用类似于typedef(类型定义) 名称。 例如, 如果T是一个模板参数, 就不能使用诸如class T等形式的修饰名称, 即使T是一个要被class类型替换的参数也不可以:
template
class List
{
class Allocator* allocator; //错误
...
}
非类型参数表示的是: 在编译期或链接期可以确定的常值。 这种参数的类型(换句话说, 就是这些常值的类型) 必须是下面的一种:
•整型或者枚举类型。
•指针类型(包含普通对象的指针类型、 函数指针类型、 指向成员的指针类型) 。
•引用类型(指向对象或者指向函数的引用都是允许的) 。
所有其他的类型现今都不允许作为非类型参数使用(但是在将来很可能会增加浮点数类型) 。或许会令你惊讶的是, 在某些情况下, 非模板参数的声明也可以使用关键字typename:
template //非类型参数
class List;
这两种参数的区分很容易: 第 1 个 typename 的后面是一个简单标识符 T, 而第 2 个typename的后面是一个受限的名称。
函数和数组类型也可以被指定为非模板参数, 但要把它们先隐式地转换为指针类型, 这种转型也称为decay:
template class Lexer; //buf实际上是一个int*类型
template class Lexer; //正确: 这是上面的重新声明
非类型模板参数的声明和变量的声明很相似, 但它们不能具有static、 mutable等修饰符; 只能具有const和volatile限定符。 但如果这两个限定符限定的如果是最外层的参数类型, 编译器将会忽略它们:
template class Buffer;//这里的const是没用的, 被忽略了
template class Buffer; //和上面是等同的
最后, 非类型模板参数只能是右值: 它们不能被取址, 也不能被赋值。
模板的模板参数是代表类模板的占位符。 它的声明和类模板的声明很类似, 但不能使用关键字struct和union:
template class C> //正确
void f(C* p);
template struct C> //错误
void f(C* p);
template union C> //错误
void f(C* p);
模板的模板参数的参数(如下面的A) 可以具有缺省模板实参。 显然, 只有在调用时没有指定该参数的情况下, 才会应用缺省模板实参:
template class Container>
class Adaptation
{
Container storage;
//隐式等同于Container
...
};
对于模板的模板参数而言, 它的参数名称只能被自身其他参数的声明使用。 下面的假设例子说明了这一点:
template class Buf>
class Lexer
{
static char storage[5];
Buf::storage[0]> buf;
};
template class List>
class Node
{
static T* storage;
//错误: 模板的模板参数的参数在这里不能被使用
...
};
通常而言, 模板的模板参数的参数的名称(如上面例子的T)并不会在后面被用到。 因此, 该参数也经常被省略不写, 即没有命名。 例如,前面Adaptation模板的例子可以这样声明:
template class
Container>
class Adaptation
{
Container storage;
//隐式等价于Container
...
};
从前面我们知道, 任何类型的模板参数都可以拥有一个缺省实参, 只要该缺省实参能够匹配这个参数就可以。 显然, 缺省实参不能依赖于自身的参数; 但可以依赖于前面的参数:
template >
class List;
//就是说, allocator不能依赖于本身参数Allocator,
//但是能依赖于前面参数T
与缺省的函数调用参数的约束一样; 对于任一个模板参数, 只有在之后的模板参数都提供了缺省实参的前提下, 才能具有缺省模板实参。后面的缺省值通常是在同个模板声明中提供的, 但也可以在前面的模板声明中提供。
template
class Quintuple; //正确
template
class Quintuple; //正确, 根据前面的模板声明T4和T5已经具有缺省值了
template
class Quintuple; //错误, T1不能具有缺省实参因为T2还没有缺省实参
另外, 缺省实参不能重复声明:
template
class Value;
template
class Value; //错误: 重复出现的缺省实参
模板实参是指: 在实例化模板时, 用来替换模板参数的值。 我们可以使用下面几种不同的机制来确定这些值:
•显式模板实参: 紧跟在模板名称后面, 在一对尖括号内部的显式模板实参值。 所组成的整个实体称为template-id。
•注入式(injected) 类名称(如果在类本身的作用域中插入该类的名称, 我们就称该名称为插入式类名称): 对于具有模板参数P1、 P2……的类模板X, 在它的作用域中, 模板名称(即X) 等同于template-id: X
template class C
{
C* a; //正确: 等价于C* a
C b; //正确
}
•缺省模板实参: 如果提供缺省模板实参的话, 在类模板的实例中就可以省略显式模板实参。 然而, 即使所有的模板参数都具有缺省值,一对尖括号还是不能省略的(即使尖括号内部为空, 也要保留尖括号)
•实参演绎: 对于不是显式指定的函数模板实参, 可以在函数的调用语句中, 根据函数调用实参的类型来演绎出函数模板实参。
另外, 如果所有的模板实参都可以通过演绎获得, 那么在函数模板名称后面就不需要指定尖括号。
对于函数模板的模板实参, 我们可以显式指定它们, 或者借助于模板的使用方式对它们进行实参演绎。 例如:
template
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
int main()
{
max(1.0, -3.0); //显式指定模板实参
max(1.0, -3.0); //模板实参被隐式演绎成double
max(1.0, 3.0); //显式的禁止了演绎因此返回结果是int类型
}
然而, 某些模板实参永远也得不到演绎的机会 , 于是, 我们最好把这些实参所对应的参数放在模板参数列表的开始处, 从而可以显式指定这些参数, 而其他的参数仍然可以进行实参演绎。 例如:
template
inline DstT implicit_cast(SrcT const& x) //Srct可以被演绎但DstT不可以
{
return x;
}
int main()
{
double value = implicit_cast(-1);
}
如果我们调换例子中模板参数的顺序(换句话说, 我们把该模板写成: template
由于函数模板可以被重载, 所以对于函数模板而言, 显式提供所有的实参并不足以标识每一个函数: 在一些例子中, 它标识的是由许多函数组成的函数集合。 下面的例子清楚地说明了这一点:
template
void apply(Func func_ptr, T x)
{
fun_ptr(x);
}
template void single(T);
template void multi(T);
template void multi(T*);
int main()
{
apply(&single, 3); //正确
apply(&multi, 7); //错误: &multi不唯一
}
另外, 在函数模板中, 显式指定模板实参可能会试图构造一个无效的C++类型。 考虑下面的重载模板函数:
template RT1 test(typename T::X const*);
template RT2 test(...);
表达式test
显然, “替换失败并非错误(substitution-failure-is-not-an-error,SFINAE)”原则是令函数模板可以重载的重要因素。
SFINAE 原则保护的只是: 允许试图创建无效的类型, 但并不允许试图计算无效的表达式。 因此, 下面的例子是错误的C++例子:
template void f(int(&)[24 / (4 - I)]);
template void f(int(&)[24 / (4 + I)]);
int main()
{
&f < 4 > ; //错误, 替换后第一个除数等于0(不能应用SFINAE)
}
即使第2个模板支持这种替换, 它的除数也不会为0, 但是这个例子是错误的。 而且, 这种错误只会在表达式自身出现, 并不会在模板参数表达式的绑定中出现。 因此, 下面的例子是合法的:
template int g() { return N; }
template int g() { return *P; }
int main()
{
return g<1>(); //虽然数字1不能被绑定到int*参数但是应用了SFINAE原则
}
模板的类型实参是一些用来指定模板类型参数的值。 我们平时使用的大多数类型都可以被用作模板的类型实参, 但有两种情况例外:
(1) 局部类和局部枚举(换句话说, 指在函数定义内部声明的类型) 不能作为模板的类型实参。
(2) 未命名的 class 类型或者未命名的枚举类型1不能作为模板的类型实参(然而, 通过typedef声明给出的未命名类和枚举是可以作为模板类型实参的) 。
下面的例子很好地说明了这两种例外情况:
template class List
{
...
};
typedef struct
{
double x, y, z;
} Point;
struct { int x; } s;
typedef enum { red, green, blue } *ColorPtr;
int main()
{
struct Association //局部类型
{
int* p;
int* q;
};
List error1; //错误: 模板实参中使用了局部类型
List error2; //错误: 模板实参中使用了未命名的类型
//因为typedef定义的是*ColorPtr, 并非ColorPtr
List error2; //错误: 模板实参中使用了未命名的类型
List ok; //正确: 通过使用typedef定义的未命名类型
}
通常而言, 尽管其他的类型都可以用作模板实参, 但前提是该类型替换模板参数之后获得的构造必须是有效的。
template
void clear(T p)
{
*p = 0; //要求单目运算符*可以用于类型T
}
int main()
{
int a;
clear(a); //错误: int类型并不支持单目运算符*
}
非类型模板实参是那些替换非类型参数的值。 这个值必须是以下几种中的一种:
•某一个具有正确类型的非类型模板参数。
•一个编译期整型常值(或枚举值) 。 这只有在参数类型和值的类型能够进行匹配, 或者值的类型可以隐式地转换为参数类型(例如, 一个char值可以作为int参数的实参) 的前提下, 才是合法的。
•前面有单目运算符&(即取址) 的外部变量或者函数的名称。 对于函数或数组变量, &运算符可以省略。 这类模板实参可以匹配指针类型的非类型参数。
•对于引用类型的非类型模板参数, 前面没有&运算符的外部变量和外部函数也是可取的。
•一个指向成员的指针常量; 换句话说, 类似&C::m的表达式, 其中C是一个class 类型, m是一个非静态成员(成员变量或者函数) 。 这类实参只能匹配类型为“成员指针”的非类型参数。
当实参匹配“指针类型或者引用类型的参数”时, 用户定义的类型转换(例如单参数的构造函数和重载类型转换运算符) 和由派生类到基类的类型转换, 都是不会被考虑的; 即使在其他的情况下, 这些隐式类型转换是有效的, 但在这里都是无效的。 隐式类型转换的唯一应用只能是: 给实参加上关键字const或者volatile。
下面是一些有效的非类型模板实参的例子:
template
class C;
C* c1; //整型
int a;
C* c2; //外部变量的地址
void f();
void f(int);
C* c3; //函数名称: 在这个例子中, 重载解析会选择f(int),f前面的&隐式省略了
class X
{
public:
int n;
static bool b;
};
C* c4; //静态类成员是可取的变量(和函数) 名称
C* c5; //指向成员的指针常量
template
void templ_func();
C >* c6;//函数模板实例同时也是函数
模板实参的一个普遍约束是: 在程序创建的时候, 编译器或者链接器要能够确定实参的值。 如果实参的值要等到程序运行时才能够确定(譬如, 局部变量的地址),就不符合“模板是在程序创建的时候进行实例化”的概念了。
另一方面, 有些常值不能作为有效的非类型实参, 这也许会令你觉得很诧异。 这些常值包括:
•空指针常量。
•浮点型值。
•字符串。
“模板的模板实参”必须是一个类模板, 它本身具有参数, 该参数必须精确匹配它“所替换的模板的模板参数”本身的参数。 在匹配过程中, “模板的模板实参”的缺省模板实参将不会被考虑(下例中的Allocator)(但是如果“模板的模板参数”(下例中的Container)具有缺省实参, 那么模板的实例化过程是会考虑模板的模板参数的缺省实参的)。
//List的声明:
// namespace std {
// template >
// class list;
// }
template class Container >
//Container期望的是只具有一个参数的模板
class Relation
{
public:
...
private:
Container dom1;
Container dom2;
};
int main()
{
Relation rel;
//错误: std::list是一个具有多个(即2个) 参数的模板
...
}
这里的问题是: 标准库中的std::list模板具有两个参数, 它的第2个参数(我们称之为内存配置器allocator) 具有一个缺省值; 但是当我们匹配std::list和Container参数时, 事实上并不会考虑这个缺省值(即认为缺省值并不存在)。也就是说模板的模板实参(譬如这里的 std::list)是一个具有参数 A 的模板, 它将替换模板的模板参数(譬如这里的
Container) , 而模板的模板参数是一个具有参数B的模板; 匹配过程要求参数A和参数B必须完全匹配; 然而在这里, 我们并没有考虑模板的模板实参的缺省模板参数, 从而也就使B中缺少了这些缺省参数值, 当然就不能获得精确的匹配。
有时, 我们可以通过给模板的模板参数添加一个具有缺省值的参数, 来解决这个问题。 在前面的例子中, 我们可以这样改写Relation模板:
template > class Container>
//Container现在就能够接受一个标准容器模板了
class Relation
{
public:
...
private:
Container dom1;
container dom2;
};
另外我们注意到了一个事实: 从语法上讲, 只有关键字class才能被用来声明模板的模板参数; 但是这并不意味只有用关键字 class 声明的类模板才能作为它的替换实参。 实际上, “struct模板”、 “union模板”都可以作为模板的模板参数的有效实参。 这和我们前面所提到的事实很相似: 对于用关键字class声明的模板类型参数, 我们可以用(满足约束的) 任何类型作为它的替换实参。
当每个对应实参值都相等时, 我们就称这两组模板实参是相等的。对于类型实参, typedef名称并不会对等价性产生影响; 就是说, 最后比较的还是typedef原本的类型。 对于非类型的整型实参, 进行比较的是实参的值; 至于这些值是如何表达的, 也不会产生影响。 下面的例子说明了这一点:
template
class Mix;
typedef int Int;
Mix* p1;
Mix* p2; //p2和p1的类型是相同的
另外, 从函数模板产生(即实例化出来) 的函数一定不会等于普通函数, 即便这两个函数具有相同的类型和名称。 这样, 针对类成员, 我们可以引申出两点结论:
(1) 从成员函数模板产生的函数永远也不会改写一个虚函数(进一步说明成员函数模板不能是一个虚函数) 。
(2) 从构造函数模板产生的构造函数一定不会是缺省的拷贝构造函数(类似, 从赋值运算符模板产生的赋值运算符也一定不会是一个拷贝赋值运算符。 但是, 后面这种情况通常不会出现问题, 因为与拷贝构造函数不同的是: 赋值运算符永远也不会被隐式调用) 。
友元声明的基本概念是很简单的: 授予“某个类或者函数访问友元声明所在的类”的权利。 然而, 由于以下两个事实, 这些简单概念却变得有些复杂:
(1) 友元声明可能是某个实体的唯一声明。
(2) 友元函数的声明可以是一个定义。
友元类的声明不能是类定义, 因此友元类通常都不会出现问题。 在引入模板之后, 友元类声明的唯一变化只是: 可以命名一个特定的类模板实例为友元。
template
class Node;
template
class Tree
{
friend class Node;
...
};
显然, 如果要把类模板的实例声明为其他类(或者类模板) 的友元, 该类模板在声明的地方必须是可见[的。 然而, 对于一个普通类, 就没有这个要求:
template
class Tree
{
friend class Factory; //正确: 即使这里是Factory的首次声明
friend class Node; //如果Node在此是不可见的这条语句就是错误的
};
通过确认紧接在友元函数名称后面的是一对尖括号, 我们可以把函数模板的实例声明为友元。 尖括号可以包含模板实参, 但也可以通过调用参数来演绎出实参。 如果全部实参都能够通过演绎获得的话, 那么尖括号里面可以为空:
template
void combine(T1,T2);
class Mixer
{
friend void combine<>(int&, int&); //正确: T1 = int&, T2 = int&
friend void combine(int, int); //正确: T1 = int, T2 = int
friend void combine(char, int); //正确: T1 = char, T2 = int
friend void combine(char&, int); //错误: 不能匹配上面的combine()模板
friend void combine<>(long, long) { ...} //错误: 这里的友元声明不允许出现定义。
}
另外应该知道: 我们不能在友元声明中定义一个模板实例(我们最多只能定义一个特化) ; 因此, 命名一个实例的友元声明是不能作为定义的。
如果名称后面没有紧跟一对尖括号, 那么只有在下面两种情况下是合法的:
(1) 如果名称不是受限的(就是说, 没有包含一个形如双冒号的域运算符) , 那么该名称一定不是(也不能) 引用一个模板实例。 如果在友元声明的地方, 还看不到所匹配的非模板函数(即普通函数) , 那么这个友元声明就是函数的首次声明。 于是, 该声明可以是定义。
(2) 如果名称是受限的(就是说前面有双冒号::),那么该名称必须引用一个在此之前声明的函数或者函数模板。 在匹配的过程中,匹配的函数要优先于匹配的函数模板。 然而, 这样的友元声明不能是定义。
简单来说,名称不是受限,只能是函数,可以是首次声明,则可以是定义。名称是受限的,可以是模板或者函数,优先函数,不能是定义。
下面的例子可以说明这些情况:
void multiply(void*); //普通函数
template
void multiply(T); //函数模板
class Comrades {
friend void multiply(int) { }//定义了一个新的函数::multiply(int)//非受限函数名称, 不能引用模板实例
friend void ::multiply(void*);//引用上面的普通函数,不会引用multiply实例
friend void ::multiply(int);//引用一个模板实例
friend void ::multiply(double*)//受限名称还可以具有一对尖括号,但模板在此必须是可见的
friend void ::error() { }//错误: 受限的友元不能是一个定义
};
在前面的例子中, 我们是在一个普通类里面声明友元函数。 如果需要在类模板里面声明友元函数, 前面的这些规则仍然是适用的, 唯一的区别就是: 可以使用模板参数来标识友元函数。
template
class Node
{
Node* allocate();
...
};
template
class List
{
friend Node* Node::allocate();
...
};
然而, 如果我们在类模板中定义一个友元函数, 那么将会出现一个很有趣的现象。 因为对于任何只在模板内部声明的实体, 都要等到模板被实例化之后, 才会是一个具体的实体; 在这之前该实体是不存在的。类模板的友元函数也是如此。 考虑下面的例子:
template
class Creator {
friend void appear()
{ //一个新函数::appear()
... //但要等到Creator被实例化之后/才存在
}
};
Creator miracle; //这时才生成::appear()
Creator oops; //错误: ::appear()第2次被生成
在这个例子中, 两个不同的实例化过程生成了两个完全相同的定义(即 appear 函数) , 这违反了ODR原则(一处定义原则) 。因此, 我们必须确定: 在模板内部定义的友元函数的类型定义中,必须包含类模板的模板参数(除非我们希望在一个特定的文件中禁止多于一个的实例被创建, 但这种用法很少) 。 让我们这样修改前面的例子:
template
class Creator
{
friend void feed(Creator*) { //每个T都生成一个不同的::feed()函数
...
}
};
Creator one; //生成::feed(Creator*)
Creator two; //生成: : feed(Creator*)
在这个例子中, 每个Creator的实例都生成了一个不同的feed()函数。 另外我们应该知道: 尽管这些函数是作为模板的一部分被生成的,但函数本身仍然是普通函数, 而不是模板的实例。最后一点就是: 由于函数的实体处于类定义的内部, 所以这些函数是内联函数。 因此, 在两个不同的翻译单元中可以生成相同的函数。
我们通常声明的友元只是: 函数模板的实例或者类模板的实例, 我们指定的友元也只是特定的实体。 然而, 我们有时候需要让模板的所有实例都成为友元, 这就需要声明友元模板。 例如:
class Manager
{
template
friend class Task; //友元类模板
template
friend void Schedule::dispatch(Task*); //友元类成员函数模板
template
friend int ticket() //友元函数模板
{
return ++Manager::counter;
}
static int counter;
};
和普通友元的声明一样, 只有在友元模板声明的是一个非受限的函数名称, 并且后面没有紧跟尖括号的情况下, 该友元模板声明才能成为定义。
友元模板声明的只是基本模板和基本模板的成员。 当进行这些声明之后, 与该基本模板相对应的模板局部特化和显式特化都会被自动地看成友元。