C++基础-模板和泛型编程

模板和泛型编程

模板是c++泛型编程的基础。

定义模板

函数模板

1
2
3
4
5
6
7
template <typename T>
int compare(const T &v1, const T &v2)
{
4if (v1 < v2) return -1;
4if (v1 > v2) return 1;
4return 0;
}

模板定义以关键字template开始,后跟一个模板参数列表,模板参数列表不能为空。

模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(显式或者隐式地)指定模板参数,将其绑定到模板参数上。

当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实参。即,编译器使用实参的类型来确定绑定到模板实参T的类型。

编译器用推断出的模板参数来实例化出一个特定版本的函数,该特定版本的函数称为模板的实例

模板类型参数

在模板参数列表中用于定义类型的参数称为模板类型参数。

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

类型参数前必须使用关键字class或typename,在模板参数列表中,这两个关键字的含义相同,可以互换使用。

非类型模板参数

一个非类型模板参数表示一个值而非一个类型。通过一个特定的类型名而非关键字class或typename来制定非类型参数。

当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所替代。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。

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

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

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

模板编译

当编译器遇到一个模板定义时,它并不生成代码,只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。

通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而将普通函数和类的成员函数的定义放在源文件中。

模板不同,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此,与非模板代码不同,模板的头文件通常包括声明也包括定义

大多数编译错误在实例化期间报告

编译器会在三个截断报告错误:

  1. 编译模板本身时。
  2. 编译器遇到模板使用时,该阶段检查模板实参数目是否正确、参数类型是否匹配,可检查的错误很少。
  3. 模板实例化时,只有这个阶段才能发现类型相关的错误。

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能够正确工作,是调用者的责任。

类模板

类模板作为类的蓝图,与函数模板不同,编译器无法为类模板推断模板参数类型,在使用类模板时,我们必须在类模板名后的尖括号内提供额外信息。

类模板的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template <typename T> class Blob{
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Blob();
Blob(std::initializer_list<T> i1);
// Blob中的元素数目
size_type size() const { return data->size(); }
bool empty() const { return data->empty; }
// 添加和删除元素
void push_back(const T &t) { data->push_back(t); }
// 移动版本
void push_back(T &&t) { data->push_back(std::move(t)); }
void pop_back();
// 元素访问
T &back();
T &operator[] (size_type i);
private:
std::shared_ptr<std::vector<T>> data;
// 若data[i]无效,则抛出msg
void check(size_type i, const std::string &msg) const;
}

在定义中有下面一句代码:

1
typedef typename std::vector<T>::size_type size_type;

其中,typename的作用是,在实例化模板之前,编译器不清楚std::vector<T>的具体类型,使用typename告诉编译器后者是一个类型。

在上述类模板的构造函数中,使用了如下的格式:

1
Blob(std::initializer_list<T> i1);

在上述语句中使用了标准库中的initializer_list类模板,该类可以使得我们使用下述语句对类进行初始化:

1
Blob<int> ia2 = {0,1,2,3,4};

实例化类模板

当编译器从Blob模板实例化出一个类时,它会重写Blob模板,将模板参数T的每个实例替换为给定的模板实参。

一个类模板的每个实例都形成一个独立的类。

在模板作用域中引用模板类型

类模板的名字不是一个类型名,类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。

一个类模板中的代码如果使用了另外一个模板,通常不将一个实际类型(或值)的名字用作其模板实参。通常将模板自己的参数当做被使用模板的实参。

无论何时使用模板都必须提供模板实参,提供给类模板中的模板的模板实参就是使用它的类模板的模板参数。

类模板的成员函数

类模板的成员函数具有和类模板相同的模板参数,因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。

在类外定义一个成员时,必须说明成员属于哪个类。而且,从一个模板生成的类的名字中必须包含其模板实参。当我们定义一个成员函数时,模板实参与模板形参相同。

1
2
template <typename T>
ret-type Blob<T>::member-name(parm-list);

例如,上述类模板的check成员。

1
2
3
4
5
6
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
4if (i >= data->size())
44throw std::out_of_range(msg);
}

类模板的构造函数

与其他的在外部的类模板的普通成员函数一样,构造函数的定义要以模板参数开始。

1
2
template <typename T>
Blob<T>::Blob():data(std::make_shared<std::vector<T>>()) {}

类模板成员函数的实例化

一个类模板的成员函数只有当程序用到它时才进行实例化。如果一个成员函数没有被使用,则它不会被实例化,这一特性使得即使某种类型不能完全符合模板操作的要求。

 在类代码内简化模板类名的使用

在类模板自己的作用域内,可以直接使用模板名而不提供模板实参。当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了模板参数匹配的实参一样。

在类模板外使用类模板名

当我们在类模板为定义其成员时,必须记住,我们并不在类的作用域内,只有遇到类名才表示进入类的作用域。

1
2
3
4
5
6
7
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
BlobPtr ret = *this; // 保存当前状态
++*this; // 推进一个元素;前置++检车递增是否合法
return ret; // 返回保存的状态
}

返回类型位于类的作用域之外,因此必须指定模板类型参数。在函数体内,已经进入了类的作用域,因而在定义ret时无需重复模板实参。

类模板和友元

  • 如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。
  • 如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系

类模板与另一个模板(类模板或函数模板)友好关系的常见形式是建立对应实例及其友元间的友好关系。

例如,Blob类将BlobPtr类和一个模板版本的Blob相等运算符定义为友元的情况。

为了引用(类或函数)模板的一个特定实例,我们必须首先声明模板自身。一个模板的声明包括模板参数列表。

1
2
3
4
5
6
7
8
9
10
11
12
// 前置声明在Blob中声明友元所需
template <typename T> class BlobPtr;
template <typename T> calss Blob; // 运算符==中的参数需要
template <typename T>
4bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob{
// 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator==<T>
(const Blob<T>&, const Blob<T>&);
}

在上述代码中,友元的声明用Blob的模板形参作为它们自己的模板实参。因此,友好关系被限定在用相同类型实例化的Blob与BlobPtr相等运算符之间。

通用和特定的模板友好关系

一个类可以将另一个模板的每个实例都声明为自己的友元,也可以限定特定的实例为友元。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T> class Pal;
class C{
friend class Pal<C>;// 用类C实例化的Pal是C的一个友元
template <typename T> friend class Pal2; // 模板类Pal2的所有实例都是C的友元,这种情况无需前置声明
};

template <typename T> class C2{
// C2的每个实例将相同实例化的Pal声明为友元,需要前置声明
friend class Pal<T>;
template <typename X> friend class Pal2;// 模板类Pal2的所有实例都是C的友元
// Pal3是非模板类,它是C2所有实例的友元
friend class Pal3;
}

为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数

模板类型别名
1
typedef Blob<string> StrBlob;
类模板的static成员

类模板可以声明static成员.。

1
2
3
4
5
6
template <typename T> class Foo{
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
}

类模板的不同实例有其各自的static成员实例。所有Foo<X>类型的对象共享相同的ctr对象和count函数。

模板类的每个static数据成员都有且仅有一个定义,但是,类模板的每个实例都有一个独有的static对象,因而,将类模板的static数据成员也定义为模板:

1
2
template <typename T>
size_t Foo<T>::ctr = 0;