C++ 模板与泛型编程 (定义模板) 学习笔记
1.1 定义模板
假如我们希望编写一个函数来比较两个值,但是在使用过程中有时候需要比较int
、double
、string
或者自定义的class
。那么可能需要重载若干个函数来实现程序。
1.1.1 函数模板
我们可以定义通用的函数模板来实现
模板定义以关键字template
开始,后跟一个模板参数列表(可有多个模板参数),这是一个逗号分隔的一个或多个模板参数的列表,用< >
包围。
注意:在模板定义中,模板参数列表不能为空。
例如:
template<typename T1, typename T2>
模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者(隐式地或显式地)指定模板实参,将其绑定到模板参数上。
实例化函数模板
当我们调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例函数”。
模板类型参数
一般来说,我们可以将模板函数的类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:
template <typename T>
T foo(T* p)
{
T tmp = *p;
// ...
return tmp;
}
注:类型参数钱必须使用关键字class
或typename
,在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表中可以同时使用这两个关键字:
template<typename T, class U>
calc(const T&, const U&);
非类型模板参数
一个非类型参数表示一个值而非一个类型,我们通过一个特定的类型名而非关键字class
或typename
来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式,从而允许编译器在编译时候实例化模板。
例如,我们编写一个compare
版本处理字符串字面常量。这种字面常量是const char
的数组。由于不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。由于我们希望能比较不同长度的字符串字面常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度:
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
当我们调用这两个版本的compare时:
compare("hi", "mom");
编译器回使用字面常量的大小来代替N
和M
,从而实例化模板。记住,编译器会在一个字符串字面常量的末尾插入一个空字符串作为终结符,因此编译器回实例化出如下版本:
一个非类型参数可以是一个整型
,或者是一个指向对象或函数类型的指针
或(左值)引用
。
- 绑定到非类型整型参数的实参必须是一个常量表达式。
- 绑定到指针或引用非类型参数的实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。
- 指针参数也可以用
nullptr
或一个值为0的常量表达式来实力哈哈哈。
在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
注:非类型模板参数的模板实参必须是常量表达式。
inline和constexpr的函数模板
函数模板可以声明为inline或constexpr,两个说明符放在模板参数列表之后,返回类型之前:
模板编译
当编译器遇到一个模板定义时, 它并不生成代码。只有当我们实例化出模板的一个特定版本时, 编译器才会生成代码。
通常, 当我们调用一个函数时, 编译器只需要掌握函数的声明。类似的, 当我们使用一个类类型的对象时, 类定义必须是可用的, 但成员函数的定义不必已经出现。因此, 我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同: 为了生成一个实例化版本, 编译器需要掌握函数模板或类模板成员函数的定义。因此, 与非模板代码不同, 模板的头文件通常既包括声明也包括定义。
注:函数模板和类模板成员函数的定义通常放在同一个头文件中
大多数编译错误在实例化期间报告
模板知道实例化时才会生成代码,这一特性影响了我们合适才会获知模板内代码的编译错误。百年一起会在三个阶段报告错误:
- 编译模板本身的语法错误。
- 编译器遇到模板使用时,检查给定的实参是否正确。
- 模板实例化时,发现类型相关的错误。
注:保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
1.1.2 类模板
类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。
定义类模板
定义一个模板类:
实例化类模板
我们必须使用显式模板实参列表进行实例化:
注:一个类模板的每个实例都形成一个独立的类。
在模板作用域中引用模板类型
一个类模板中的代码如果使用了另外一个模板,通常将模板自己的参数当作被使用模板的实参,例如:
类模板的成员函数
与其他任何类相同,我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义的类模板内的成员函数被隐式声明为内联函数。
定义在类模板之外的成员函数必须以关键字template
开始,后接类模板参数列表。
通用的格式如下:
具体代码如下:
模板类的构造函数
最基本的形式:
另一种方法是,接受一个initializer_list
参数的构造函数将其类型函数T
作为initializer_list
参数的元素类型:
为了使用这个构造函数,我们必须传递给它一个initializer_list
,其中的元素必须与Blob的元素类型兼容:
类模板成员函数的实例化
一个类模板的成员函数只有被使用时才进行实例化:
这一特性使得及时某种类型不能完全符合模板操作的要求,我们仍然能用该类型实例化类。
在类代码内简化模板类名的使用
在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。例如:
template <typename T>
class treeNode
{
public:
T val;
treeNode* next;
public:
treeNode(T val_):val(val_) {}
};
在类模板外使用类模板名
由于返回类型尾与类的作用域之外,我们必须显式地指出类模板中的参数类型它所用类型与类实例化所用类型一致。
在函数体内,我们以及进入类的作用域,因此在定义ret
时无须重复模板实参。如果不提供模板实参,则编译器回假定我们使用的类型与成员实例化所用类型一致。因此,ret
的定义与如下代码等价:
类模板的static成员
每个Foo的实例都有自己独立的static成员实例。
1.1.3 模板参数
模板参数与作用域
一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。模板参数回隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次:
模板声明
模板声明必须包含模板参数:
与函数参数相同,声明中的模板参数的名字不必与定义中相同:
不过,一个给定模板的每个声明和定义必须有相同数量和种类的参数。
使用类的类型成员
如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字typename
来实现:
注:当我们希望通知编译器一个名字表示类型时,必须使用关键字typename
,而不是clase
。
默认模板实参
例如,我们重写compare
,默认使用标准库的less
函数对象模板
当用户调用这个版本的compare
时,可以提供自己的比较操作,但这并不是必须的:
模板默认实参与类模板
无论合适使用一个类模板,我们都必须在模板名之后街上尖括号。尖括号指出类必须从一个模板实例化而来。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
1.1.4 成员模板
普通(非模板)类的成员模板
类模板的成员模板
实例化与成员模板
为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参。我们在哪个对象上调用成员模板,编译器就根据该对象的类型来推断类模板参数的实参: