C++学习——模板与泛型编程

C++学习——模板与泛型编程

  • 定义模板
    • 函数模板
    • 类模板
    • 模板参数
    • 成员模板
    • 控制实例化
  • 模板实参推断
    • 类型转换与模板类型参数
    • 函数模板显式实参
    • 函数指针和实参推断

定义模板

函数模板

可以定义一个通用的函数模板,而不是为每个类型都定义一个新函数。

一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。

compare 的模板版本可能像下面这样:

template <typename T>
int compare(const T &v1, const T &v2)
{
	if (v1 < v2) return -1;
	if (v2 < v1) return 1;
	return 0;
}

模板定义以关键字 template 开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用小于号(<)和大于号(>)包围起来。

当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。

compare 函数声明了一个名为 T 的类型参数。在 compare 中,我们用名字 T 表示一个类型。而 T 表示的实际类型则在编译时根据 compare 的使用情况来确定。

实例化函数模板

当调用一个函数模板时,编译器(通常)用函数实参来为我们推断模板实参。

编译器用推断出的模板参数来为我们实例化一个特定版本的函数。
当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。

模板类型参数

可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型换行:

// 正确:返回类型和参数类型相同
template <typename T> T foo(T* p)
{
	T tmp = *p;   // tmp 的类型将是指针p指向的类型
	// ...
	return tmp;
}

非类型模板参数

还可以在模板中定义非类型参数。
一个非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字 class 或 typename 来指定非类型参数。

当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。

一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。

绑定到非类型整型参数的实参必须是一个常量表达式。
绑定到指针或引用非类型参数的实参必须具有静态的生存期。

在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。

inline 和 constexpr 的函数模板

函数模板可以声明为 inline 或 constexpr 的,如同非模板函数一样。
inline 或 constexpr 说明符放在模板参数列表之后,返回类型之前。

函数模板和类模板成员函数的定义通常放在头文件中。

通常编译器会在三个阶段报告错误。

  • 第一个阶段是编译模板本身时。
  • 第二个阶段是编译器遇到模板使用时。
  • 第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。

类模板

类模板是用来生成类的蓝图的。

为了使用类模板,必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。

定义类模板

类模板以关键字 template 开始,后跟模板参数列表。

在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值。

template <typename T> class <类名>
{
<类成员声明>
};

实例化类模板

使用类模板时,必须提供额外信息,这些额外信息是显式模板实参列表,它们绑定到模板参数。

模板参数

一个模板参数的名字也没有什么内在含义。
通常将类型参数命名为 T,但实际上可以使用任何名字。

template <typename Foo> Foo calc(const Foo&, const Foo& b)
{
	Foo tmp = a;    // tmp 的类型与参数和返回类型一样
	// ...
	return tmp;     // 返回类型和参数类型一样
}

模板参数与作用域

模板参数遵循普通的作用域规则。

一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。

在模板内不能重用模板参数名:

typedef double A;
template <typename A, typename B> void f(A a, B b)
{
	A tmp = a;   // tmp 的类型为模板参数A的类型,而非 double
	double B;    // 错误:重声明模板参数B
}

模板声明

模板声明必须包含模板参数。

声明中的模板参数的名字不必与定义中相同。

一个给定模板的每个声明和定义必须由相同数量和种类(即,类型或非类型)的参数。

成员模板

一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。
这种成员被称为成员模板。成员模板不能是虚函数。

普通(非模板)类的成员模板

与任何其他模板相同,成员模板也是以模板参数列表开始的。

类模板的成员模板

对于类模板,也可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数。

与类模板的普通函数成员不同,成员模板是函数模板。
当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
类模板的参数列表在前,后跟成员自己的模板参数列表。

控制实例化

当模板被使用时才会进行实例化。意味着,相同的实例可能出现在多个对象文件中。
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就会有该模板的一个实例。

可以通过显式实例化来避免实例化相同模板的额外开销。

一个显式实例化有如下形式:

extern template declaration;    // 实例化声明
template declaration;           // 实例化定义

declaration 是一个类或函数声明,其中所有模板参数已被替换为模板实参。

模板实参推断

从函数实参来确定模板实参的过程被称为模板实参推断

类型转换与模板类型参数

编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。

在其他类型转换中,能在调用中应用于函数模板的包括如下两项。

  • const 转换:可以将一个非 const 对象的引用(或指针)传递给一个 const 的引用(或指针)形参。
  • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。

其他类型转换,如算术转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模板。

使用相同模板参数类型的函数形参

一个模板类型参数可以用作对各函数形参的类型。
由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。
如果推断出的类型不匹配,则调用就是错误的。

正常类型转换应用于普通函数实参

函数模板可以有用普通类型定义的参数,即,不涉及模板类型参数的类型。

这个函数实参不进行特殊处理:它们正常转换为对应形参的类型。

函数模板显式实参

指定显式模板实参

// 编译器无法推断T1,它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

每次调用 sum 时调用者都必须为 T1 提供一个显式模板实参

显式模板实参在尖括号中给出,位于函数名之后,实参列表之前:

// T1 是显式指定的,T2和T3是从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng);  // long long sum(int, long)

显式模板实参按由左至右的顺序与对应的模板参数匹配;第一个模板参数与第一个模板参数匹配,第二个实参与第二个参数匹配,依此类推。
只有尾部(最右)参数的显示模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。

正常类型转换应用于显示指定的实参

对于用普通类型定义的函数参数,允许进行正常的类型转换,出于同样的原因,对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换。

long lng;
compare(lng, 1024);          // 错误:模板参数不匹配
compare<long>(lng, 1024);    // 正确:实例化 compare(long, long)
compare<int>(lng, 1024);     // 正确:实例化 compare(int, int)

C++学习——模板与泛型编程_第1张图片

函数指针和实参推断

当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。

当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。

学习参考资料:

C++ 中文版 Primer   (第5版)

你可能感兴趣的:(C++学习,c++,开发语言,后端)