Fork me on GitHub

函数模版和类模板

前言

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。

本书第Ⅱ部分中介绍的容器、迭代器和算法都是泛型编程的例子。当我们编写一个泛型程序时,是独立于任何特定类型来编写代码的。当使用一个泛型程序时,我们提供类型或值,程序实例可在其上运行。

例如,标准库为每个容器提供了单一的、泛型的定义,如vector。我们可以使用这个泛型定义来定义很多类型的vector,它们的差异就在于包含的元素类型不同。

模版是泛型编程的基础。我们不必了解模版是如何定义的就能使用它们,实际上我们已经这样用了。

模版是C++中泛型编程的基础。一个模版就是一个创建类或函数的蓝图或者说公式。当使用一个vector这样的泛型类型,或者find这样的泛型函数时,我们提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。

定义模版

函数模板

我们可以定义一个通用的函数模板(function template),而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本。compare的模板版本可能像下面这样:

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

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

note: 在模板定义中,模板参数列表不能为空。

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

T表示一个类型。而T表示的实际类型则在编译时根据compare的使用情况来确定。

实例化函数模板

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

1
cout << compare(1,0) <<endl; //T为int

实参类型是int。编译器会推断出模板参数为int,并将它绑定到模板参数T。

编译器用推断出的模板参数来为我们实例化(instantiate)一个特定版本的函数。

如上调用将实例化:

1
2
//实例化出 int compare(const int&, const int&) { ...; }
cout << compare(1,0) <<endl; //T为int

模板类型参数

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

1
2
3
4
5
6
7
//正确:返回类型和参数类型相同
template <typename T>
T foo(T *p){
T tem = *p;
//...
return temp;
}

类型参数前必须使用关键字typename 或 class

1
2
3
4
//错误:U之前必须加上class或typename
template <typename T,U>
//正确:在模板参数列表中,typename和class没有什么不同
template<typename T, class U>

typename是在模板已经广泛使用之后才引入C++语言的,推荐使用。

非类型模板参数

除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。

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

例如,我们可以编写一个compare版本处理字符串字面常量。这种字面常量是const char 的数组。由于我们不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。由于我们希望能比较不同长度的字符串字面常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个模板参数表示第二个数组的长度:

1
2
3
4
template<unsigned N,unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]){
return strcmp(p1,p2);
}

当我们调用这个版本的compare时:

1
compare("hi","mom")

编译器会实例化出如下版本:

1
int compare(const char (&p1)[3], const char (&p2)[4])

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

note: 非类型模板参数的模板实参必须是常量表达式。

inline 和 constexpr的函数模板

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

1
2
//正确:inline说明符跟在模板参数列表之后
template <typename T> inline T min(const T&,const T&);

编写类型无关的代码

我们最初的compare函数虽然简单,但它说明了编写泛型代码的两个重要原则:

  • 模板中的函数参数是const的引用。
  • 函数体中的条件判断仅使用<比较运算。

大多数类型都允许拷贝,但是,不允许拷贝的类类型也是存在的。通过设置为引用,保证这写类型可以处理。而且,当处理大对象时,这种设计策略还能使函数运行得更快。

注意: 模板程序应该尽量减少对实参类型的要求。

模板编译

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

note: 函数模板和雷模板成员函数的定义通常放在头文件中

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

示例:定义自己版本的begin和end,同时编写一个constexpr模板,返回给定数组的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <string>
using namespace std;
// the same as std::begin
template<typename T, unsigned size>
T* begin_def(T(&arr)[size]){ //arr数组的引用
return arr;
}
// the same as std::end
template<typename T, unsigned size>
T* end_def(T(&arr)[size]){
//We usually don't use a function name which is the same as the function of standard libary
//This should not be const
return arr + size;
}
template<typename T,unsigned size>
constexpr unsigned getSize(const T(&arr)[size]){
return size;
}
int main()
{
string s[] = { "sssss","ss","ss","ssssszzzz" };
cout << *(begin_def(s)) << endl;
cout << *(end_def(s) - 1) << endl;
string s1[] = { "sss" };
cout << getSize(s1) << endl; //1
char c[] = "s";
cout << getSize(c) << endl; //2
return 0;
}

类模板

类模板(class template)是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息—-用来代替模板参数的模板实参列表。

定义类模板

类似函数模板,类模板以关键字template开始,后跟模板参数列表。在类模板(及其成员)的定义中,我们将模板参数当作替身,代替使用模板时用户需要提供的类型或值。

实例化类模板

一个类模板的每个实例都形成一个独立的类。类型Blob与任何其他Blob类型都没有关联,也不会对任何其他Blob类型的成员有特殊访问权限。

类模板的成员函数

类模板的成员函数本身是一个普通函数。但是,类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数列表。

对应的Blob的成员应该是这样的:

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

类模板成员函数的实例化

默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。

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

当我们使用一个类模板类型时必须提供模板参数,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参

在类模板外使用类模板名

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

1
2
3
4
5
6
7
8
//重载BlobPtr<T>模板类的后置递增运算符
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) { //类外,返回类型BlobPtr<T>
//类内,BlobPtr无须提供模板参数,写成 BlobPtr 等价 BlobPtr<T> ret = *this;
BlobPtr ret = *this;
++*this;
return ret; //返回保存的类型
}

类模板完整示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <string>
#include <vector>
#include <memory>
using namespace std;
template <typename T>
class Blob{ //typedef int size;
public:
typedef T value_type;
typedef typename vector<T>::size_type size_type;
//构造函数
Blob();
Blob(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(move(t)); }
void pop_back();
//元素访问
T& back();
T& operator[] (size_type i);
private:
shared_ptr<vector<T>> data;
//若data[i]无效,则抛出msg
void check(size_type i,const string &msg) const;
};
template <typename T>
T& Blob<T>::back(){
check(0,"subscript out of range");
return data->back();
}
template <typename T>
T& Blob<T>::operator[](size_type i) {
check(i,"subscript out of range");
(*data)[i];
}
template <typename T>
void Blob<T>::pop_back(){
check(0,"pop_back on empty Blob");
data->pop_back();
}
template <typename T>
Blob<T>::Blob():data(make_shared<vector<T>>()) {}
template <typename T>
Blob<T>::Blob(initializer_list<T> i1): data(make_shared<vector<T>>(i1)) {}
template <typename T>
void Blob<T>::check(size_type i,const string &msg) const{
if(i>=data->size()) throw out_of_range(msg);
}
int main()
{
Blob<int> ib = {1,2,3,4,5,6};
int back = ib.back();
cout<<back<<endl;
cout<<ib.size()<<endl;
return 0;
}

在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板参数。

类模板和友元(暂时不看)

类模板的static成员

与任何其他类相同,类模板可以声明static成员

1
2
3
4
5
6
7
8
9
template <typename T>
class Foo{
public:
static size_t count() { return ctr; }
//其他接口成员
private:
static size_t ctr;
//其他实现成员
};

每个Foo的实例都有其自己的static成员实例。即,对于任意给定类型X,都有一个Foo::ctr和一个Foo::count成员。所有Foo类型的对象共享相同的ctr对象和count对象。

我们将static数据成员也定义为模板:

1
2
template <typename T>
size_t Foo<T>::ctr = 0; //定义并初始化ctr

访问类模板的static成员

1
2
3
4
Foo<int> fi; //实例化Foo<int>类和static数据成员ctr
auto ct = Foo<int>:count(); //实例化Foo<int>::count
ct = fi.count(); //使用Foo<int>::count
ct = Foo::count(); //错误:使用哪个模板实例的count?

类似于任何其他成员函数,一个static成员函数只有在使用时才会实例化。

-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/12/01/函数模版和类模板/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!