到目前为止,我们已经介绍了 C++ 中模板的基本概念。在深入细节之前,让我们看看我们使用的术语。这是必要的,因为在 C++ 社区内部(甚至在早期版本的标准中),有时对于术语的使用缺乏精确性。
在 C++ 中,结构体、类和联合体统称为类类型(class types)。在没有其他限定的情况下,普通文本类型中的类(class)是指包括使用关键字 class
或 ``struct`[1] 引入的类类型。特别注意,类类型包括联合体,但类不包括。
关于如何称呼一个模板类,存在一些混淆:
-
类模板一词表明该类是一个模板,即它是一个类族的参数化描述。
-
模板类一词
- 被用作类模板的同义词
- 同时也指从模板生成的类
- 以及指名称为模板标识(模板名后跟指定在 < 和 > 之间的模板参数)的类。
第二种和第三种含义之间的区别比较微妙,对本文的其余部分并不重要。
因此,我们在本书中避免使用模板类这一术语。类似地,我们使用函数模板、成员模板、成员函数模板和变量模板,而避免使用模板函数、模板成员、模板成员函数和模板变量。
在处理使用模板的源代码时,C++ 编译器必须在不同的时间将具体模板参数替换为模板中的模板参数。有时,这种替换只是暂时的:编译器可能需要检查替换是否有效。
实际上,通过用具体参数替换模板参数来创建常规类、类型别名、函数、成员函数或变量的定义的过程称为模板实例化。
令人惊讶的是,目前没有标准或普遍认可的术语来表示,通过模板参数替换进行的创建的定义,不是声明过程。我们见过一些团队使用部分实例化(partial instantiation)或声明的实例化(instantiation of a declaration)这样的短语,但这些并不普遍。也许更直观的术语是不完整实例化(incomplete instantiation,在类模板的情况下,产生不完整类)。
通过实例化或不完整实例化(即类、函数、成员函数或变量)得到的实体统称为特化(specialization)。
然而,在 C++ 中,实例化过程并不是产生特化的唯一方法。替代机制允许程序员明确指定与模板参数的特殊替换相关联的声明。如我们在第 2.5 节中所示,这种特化用前缀 template<>
引入:
template<typename T1, typename T2> // 主要类模板
class MyClass {
...
};
template<> // 显式特化
class MyClass<std::string, float> {
...
};
严格来说,这被称为显式特化(explicit specialization,与实例化或生成特化相对)。
如第 2.6 节所述,仍然具有模板参数的特化称为部分特化(partial specializations):
template<typename T> // 部分特化
class MyClass<T, T> {
...
};
template<typename T> // 部分特化
class MyClass<bool, T> {
...
};
在谈论(显式或部分)特化时,通常的模板也被称为主模板(primary template)。
到目前为止,本书中只提到过几次声明(declaration)和定义(definition)这两个词。然而,在标准 C++ 中,这两个词有着相当精确的含义,我们采用的也是这一含义。
声明是一种C++结构,用于在 C++ 作用域中引入或重新引入一个名称。这种引入总是伴随着对该名称的部分分类,但不需要详细信息来使声明有效。例如:
class C; // 声明 C 为一个类
void f(int p); // 声明 f() 为一个函数,p 为一个有名参数
extern int v; // 声明v为一个变量
请注意,即使它们有名称,宏定义和 goto
标签在 C++ 中并不被视为声明。
当结构的详细信息被揭示,或者在变量的情况下,需要分配存储空间时,声明会变成定义。对于类类型的定义,这意味着需要提供一个大括号包围的主体。对于函数定义,通常也需要提供一个大括号包围的主体,或者该函数必须被指定为 = default
2 或 = delete
。对于变量来说,初始化或没有 extern
说明符的情况都会使声明变成定义。以下是与前面的非定义声明相补充的定义示例:
class C {}; // 类C的定义(也是声明)
void f(int p) { // 函数f()的定义(也是声明)
std::cout << p << '\n';
}
extern int v = 1; // 初始化使其成为 v 的定义
int w; // 未使用extern前缀的全局变量声明也是定义
通过扩展,如果类模板或函数模板具有主体,则其声明也称为定义。因此:
template<typename T>
void func(T); // 这是声明,不是定义
而:
template<typename T>
class S {}; // 这是定义
类型可以是完整的或不完整的,这一概念与声明和定义的区别密切相关。某些语言结构需要完整类型,而另一些则对不完整类型(incomplete types)也是有效的。
不完整类型包括以下几种:
- 已声明但尚未定义的类类型;
- 边界未指定的数组类型;
- 元素类型不完整的数组类型;
void
类型;- 如果基础类型或枚举值未定义的枚举类型;
- 对以上任意类型应用
const
和或volatile
;
所有其他类型都是完整的。例如:
class C; // C 是不完整类型
C const* cp; // cp 是指向不完整类型的指针
extern C elems[10]; // elems 是不完整类型
extern int arr[]; // arr 是不完整类型
...
class C { }; // C 现在是完整类型,因此 cp 和 elems 不再指向不完整类型
int arr[10]; // arr 现在是完整类型
关于如何在模板中处理不完整类型的提示,请参阅第 11.5 节。
C++ 语言定义对各种实体的重新声明有一些约束。这些约束的总和被称为单定义规则(one-definition rule, ODR)。该规则的细节比较复杂,涵盖了多种情况。后续章节会在每个适用的上下文中说明各种不同的方面,完整描述可见附录 A。
目前,只需记住以下ODR基础知识:
- 普通的(即非模板的)非内联函数和成员函数,以及(非内联的)全局变量和静态数据成员应在整个程序[3]中只定义一次;
- 类类型(包括结构体和联合体)、模板(包括部分特化但不包括全特化)、内联函数和变量应在每个翻译单元中至多定义一次,且所有这些定义应相同;
翻译单元是源文件经过预处理的结果,包括 #include
指令引用的内容和宏扩展生成的内容。
在本书的其余部分中,可链接实体指的是以下任何一种:函数或成员函数、全局变量或静态数据成员,包括从模板生成的任何此类实体,这些实体对链接器是可见的。
比较以下类模板:
template<typename T, int N>
class ArrayInClass {
public:
T array[N];
};
和类似的普通类:
class DoubleArrayInClass {
public:
double array[10];
};
如果将模板的参数 T
和 N
分别替换为 double
和 10
,后者就基本等同于前者。在 C++ 中,这种替换的名称表示为:
ArrayInClass<double,10>
注意,模板名称后面跟着尖括号中的模板实参。
无论这些实参是否依赖于模板形参,模板名称后跟尖括号中的参数组合被称为模板ID。这个名称可以像使用对应的非模板实体一样使用。例如:
int main() {
ArrayInClass<double,10> ad;
ad.array[0] = 1.0;
}
区分模板形参和模板实参至关重要。简单来说,你可以说形参由实参初始化。更准确地4说:
- 模板形参是在模板声明或定义中位于关键字
template
之后的那些名称(在我们的例子中是T
和N
); - 模板实参是替代模板形参的项(在我们的例子中是
double
和10
)。与模板形参不同,模板实参不仅仅是名称;
当使用模板 ID 时,模板形参被模板实参替换是显式的,但在某些情况下,这种替换是隐式的(例如,当模板形参被其默认实参替代时)。
一个基本原则是,任何模板实参必须是在编译时,可以确定的量或值。正如稍后会更清楚地说明的,这一要求带来了模板实体在运行时成本上的显著好处。因为模板形参最终被编译时的值替换,它们本身也可以用于构成编译时表达式。在 ArrayInClass
模板中,这一点得到了利用,用模板形参 N
来确定成员数组 array
的大小。数组的大小必须是一个常量表达式,而模板形参 N
符合这一要求。
我们可以更进一步推论:由于模板形参是编译时实体,它们也可以用来创建有效的模板实参。以下是一个示例:
template<typename T>
class Dozen {
public:
ArrayInClass<T,12> contents;
};
在这个示例中,名称 T
既是模板形参,又是模板实参。因此,C++ 提供了一种机制,使我们能够从简单的模板构造更复杂的模板。当然,这与我们用来组合类型和函数的机制并没有本质上的不同。
- 对于类、函数和变量的模板,分别使用类模板、函数模板和变量模板;
- 模板实例化是通过用具体实参替换模板形参来创建常规类或函数的过程。生成的实体称为特化;
- 类型可以是完整的或不完整的;
- 根据单定义规则(ODR),非内联函数、成员函数、全局变量和静态数据成员应在整个程序中只定义一次;
[1]: 在 C++ 中,类和结构体之间的唯一区别是,类的默认访问权限为私有,而结构体的默认访问权限为公共。然而,我们倾向于对使用新 C++ 特性的类型使用类,对可以用作“普通旧数据”(plaind old data,POD)的普通 C 数据结构使用结构体。
[3]: 自 C++17 起,全局和静态变量以及数据成员可以被定义为内联,这消除了它们必须在一个翻译单元中定义的要求。