模板与泛型编程
OOP,能处理类型在程序运行之前都未知的情况;泛型编程,在编译时能获取类型。
模板是泛型编程的基础。本章学习如何定义自己的模板。
16.1 定义模板
问题引出:假设希望编写一个函数来比较2个值,并指出第一个值是<, > or == 第二个值。实际编程中,可能想要定义多个重载函数,每个函数比较一种给定类型的值。这样就会写很多函数体一样的函数,而仅仅是函数类型不同,很繁琐。我们使用函数模板解决这个问题。
// 下面2个函数用于比较v1和v2的大小,仅仅是函数参数类型不一样,函数体完全一样
// string版本
int compare(const string &v1, const string &v2) {
if (v1 < v2) return -1;
else if(v1 > v2) return 1;
return 0;
}
// double版本
int compare(const double&v1, const double&v2) {
if (v1 < v2) return -1;
else if(v1 > v2) return 1;
return 0;
}
16.1.1 函数模板
以形如template
类型参数T,可以看作类型说明符,作为函数返回值类型或者形参类型。会在调用函数时,编译器利用实参类型推断出T代表的类型。
什么叫(函数模板)实例化?
当调用一个函数时,编译器用函数实参推断出的模板参数,用此实际实参代替模板参数来创建出一个新的“实例”,也就是一个真正可以调用的函数,这个过程叫实例化。
编译器生成的函数版本,通常称为模板的实例。
// 定义compare的函数模板
// compare声明了类型为T的类型参数
template <typename T> // template关键字, <typename T> 是模板参数列表,typename和class关键字等价,都可以使用,T是模板参数,以逗号分隔其他模板参数
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
else if (v1 > v2) return 1;
return 0;
}
// 调用模板,将模板实参绑定到模板参数(T)上
// 调用函数模板时,编译器根据函数实参(1,0)来推断模板实参
cout << compare(1, 0) << endl; // T为int
// 编译器会根据调用情况,推断出T为int,从而生成一个compare版本,T被替换为int
int compare(const int &v1, const int &v2) {
if (v1 < v2) return -1;
else if (v1 > v2) return 1;
return 0;
}
模板类型参数
类型参数可以看做类型说明符,像内置类型或类类型说明符一样使用,单仅限于定义模板的函数返回类型、参数类型、函数体内变量声明、类型转换。
类型参数前必须使用关键字typename或class,两者等价,可互换。(仅限于模板参数列表中)
template <typename T> T foo(T *p) {
T tmp = *p; // tmp类型T,是指针p指向的类型
// ...
return tmp;
}
// 错误使用示例
template <typename T, U> T calc(const T&, const U&); // U 之前必须加typename或class
// 正确
template <typename T, class U> T calc(const T&, const U&);
非类型模板参数
非类型参数表示一个值,而非一个类型。通过一个特定类型名而非(typename/class)来指定非类型参数。
当一个模板实例化时,非类型参数被用户提供的,或编译器推断出的值所代替。这些值必须是常量表达式。
注意:
- 绑定到非类型整型参数的实参必须是一个常量表达式;
- 绑定到指针或引用非类型参数的实参必须具有静态生存期;
template <unsinged N, unsigned M> // N, M是非类型整型参数
int compare(const char &(p1)[N], const char &(p2)[M]) {
return strcmp(p1, p2);
}
// 调用compare时,编译器会用字面量大小来替代非类型参数N和M
compare("hi", "mom"); // N = 3, M = 4,注意编译器会自动在字符串末尾添加"\0"作为终结符
inline和constexpr的函数模板
声明inline或constexpr的函数模板,inline/constexpr说明符要放在模板参数列表之后,返回类型之前:
// 正确,inline放在template模板参数之后,返回值类型之前
template <typename T> inline T min(const T&, const T&);
// 错误,inline放到了template之前
inline template <typename T> T min(const T&, const T&);
编写类型无关的代码*
前面compare函数,说明了编写泛型代码的2个重要原则:
- 模板中的函数是const引用
- 函数体中条件判断仅使用< 比较运算
但是,编写代码如果使用了 <, >运算符,就降低了compare对要处理类型的要求。也就是说,这些类型必须要支持<,>。
如果真的关心类型无关和可移植性,可能需要用到less(标准库函数,头文件 algorithm)来定义compare函数。
// 实际上less函数也用到了<,并没有起到更良好定义的作用
template <typename T> int compare(const T &v1, const T&v2) {
if (less<T>()(v1, v2)) return -1; // <=> if(v1 < v2)
else if(less<T>()(v2, v1)) return 1;
return 0;
}
模板编译*
编译器在模板定义时,不生成代码。只有实例化出模板的一个特定版本时,编译器才会生成代码。
函数模板和类模板成员函数的定义通常放在头文件中。
16.1.2 类模板
可以实例化出特定类的模板,叫类模板。
类模板是用来生成类的蓝图的。与函数模板的区别是,编译器不能为类模板推断模板参数类型。
template <typename T> class Blob { // 类型为T的模板类型参数
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Blob();
Blob(std::initializer_list<T> il);
// 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;
};
实例化类模板
要使用类模板,必须提供额外信息,即显示模板实参列表,绑定到模板参数。编译器可以用这些模板实参实例化出特定的类。
一个类模板的每个实例都是一个独立的类,比如Blob
// 使用特定类型版本的Blob(即Blob<int>),必须提供元素类型
Blob<int> ia; // 构建空Blob<int>
Blob<int> ia2 = {0,1,2,3,4}; // 构建包含5个元素的Blob<int>
// 使用Blob<string> 版本
Blob<string> names;
// 使用Blob<double> 版本
Blob<double> prices;
编译器实例化出一个与下面定义等价的类:
// 注意:所有模板参数T都被编译器根据显式模板实参,替换为对应的类型
template<> class Blob<int> {
typedef typename std::vector<int>::size_type size_type;
Blob();
Blob(std::initializer_list<int> il);
// ...
int& operator[](size_type i);
private:
std::shared_ptr<std::vector<int>> data;
void check(size_type i, const std::string &msg) const;
}
在模板作用域中引用模板类型
类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的。
也就是说,template
简而言之,就是类模板参数T,可以在类内部成员定义时使用,而T所代表的类型取决于实例化Blob
// data定义,使用了Blob的类型参数T,来声明data是一个share_ptr的实例
std::shared_ptr<std::vector<T>> data;
// 实例化特定类型Blob<string>后,data成为
shared_ptr<vector<string>> data;
类模板的成员函数
类模板的成员函数是一个普遍函数,每个实例化的类,都有自己版本的成员函数。
如check, back, operator[]
template<typename T>
void Blob<T>::check(Blob::size_type i, const std::string &msg) const { // 检查当前位置i是否合法
if (i >= data->size()) throw std::out_of_range(msg);
}
template<typename T>
T &Blob<T>::back() {
check(0, "back on empty Blob");
return data->back();
}
template<typename T>
T &Blob<T>::operator[](Blob::size_type i) {
// 如果i太大,check抛出异常,阻止访问不存在的元素
check(i, "subscripte out of range");
// return data[i]; // 错误,data是一个指向vector<T>的shared_ptr,vector下标访问需要先解引用
return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back() { // 弹出末尾元素
// 检查data指向的vector是否为空
check(0, "pop_back on empty Blob");
data->pop_back();
}
构造函数
template<typename T>
Blob<T>::Blob() : data(std::make_shared<std::vector<T>>()){ // 构造函数
}
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il) : data(std::make_shared<std::vector<T>>(il)) { // 初始化列表构造函数
}
// 使用了上面的构造函数,Blob对象就能像下面这样构造
Blob<string> articles = {"a", "an", "the"};
类模板成员函数的实例化
默认情况下,类模板成员函数只有当程序用到它时才实例化。
类内、类外使用模板类名*
类的作用域内,可以直接使用模板名而不必指定模板实参.。
// 注意模板名后面的类型参数列表<T>
// 类内可以使用简化名称
Blob &Blob(Blob &&); // 移动构造函数
Blob &operator++(); // 前置自增 <=> Blob<T> &operator()
// 类外定义成员时,不在类的作用域,要指出类型参数T
template <typename T>
Blob<T> Blob<T>::operator++(int); // 后置自增
类模板和友元
当一个模板类包含一个友元声明时,类与友元各自是否模板无关?
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。 如果友元自身是模板,类可以授权所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
引用(类或资源)模板的一个特定实例
步骤:
- 声明模板自身;
- 在类内声明友元关系;
// 注意1对1友元关系中,友元声明和类模板本身不同之处
// 前置声明,在Blob中声明友元所需要
template <typename> class BlobPtr; // 声明友元函数需要
template <typename> class Blob; // 运算符== 参数列表需要
template <typename T> bool operator==(const Blob<T> &, const Blob<T> &); // 声明要作为友元的函数
template <typename T> class Blob {
friend class BlobPtr<T>; // 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
freind bool operator==<T>(const Blob<T> &, const Blob<T> &);
//其他成员定义
...
};
通用和特定的模板友好关系
一个类将另一个类声明为友元,情况分为两大类:
1.非模板类中,声明友元类:声明的类可以是模板类,也可以是非模板类(普通友元声明);
2.模板类中,声明友元类:声明的类可以模板类,也可以是非模板类;
// 前置声明,在C和C2中声明友元所需
template <typename T> class Pal;
// 注意这里没有Pal2的前置声明
class C{ // C是一个普遍非模板类
friend class Pal<C>; // (用类C)实例化的Pal是C的一个友元,1对1友元关系。
template <typename T> friend class Pal2; // Pal2所有实例都是C的友元, 因为已经包含了模板参数列表, 不需前置声明
};
template <tyepname T> class C2 { // C2是一个模板类
friend class Pal<T>; // C2的每个实例,将相同实例化的Pal声明为友元
template <typename X> friend class Pal2; // Pal2的所有实例都是C2的友元,不需要前置声明。这里X代表Pal2使用的模板参数,跟C2使用的T不一样
friend class Pal3; // Pal3是非模板类,是C2所有实例的友元。不需要前置声明
};
令模板自己的类型参数成为友元
模板类可以将自己的类型参数,声明为友元
template <typename T> class Bar {
friend T; // 将类的访问权限,授予用来实例化的Bar类型 (模板类实例化后的类)
// ...
};
模板类型别名
用typedef定义引用实例化的类的别名,用using定义引用模板类的别名。
typedef Blob<string> StrBlob; // 正确,引用的是Blob<string>,属于模板的一个实例,StrBlob是Blob<string>的别名
typedef Blob<T> StrBlob; // 错误,由于Blob<T>模板不是一个类型,不能用typedef引用一个模板类
template <typename T> using StrBlob = Blob<T>; // 正确,StrBlob是模板类的别名
StrBlob<int> b1; // <=> Blob<int>
StrBlob<double> b2; // <=> Blob<double>
StrBlob<string> b3; // <=> Blob<string>
类模板的static成员
所有实例化的类,都包含自己的static成员。
如下面的类模板,一个给定的实例化的类Foo
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; } // static函数成员
// ...
private:
static std::size_t ctr; // static数据成员
// ...
};
16.1.3 模板参数
类似函数参数的名字,模板参数的名字只是一个符号,没有什么含义,T只是习惯上的命名。
模板参数与作用域
模板参数的作用域从声明之后,到模板声明/定义结束之前。而且,模板内不能重用模板参数名。
typedef double A;
template <typename A, typename B> void f(A a, B b) // 模板参数A,B的作用域从声明之后,到模板声明/定义结束之前
{
A tmp = a; // 覆盖了typedef对A的定义,A代表的类型由函数模板实例化决定
double B; // 错误:模板参数名不能重用
};
模板声明
声明,但不定义模板,不过必须包含模板参数。声明和定义中的参数名称,不必相同。
// 声明,但不定义模板
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
// 3个声明/定义都指向相同的函数模板
template <typename T> T calc(const T&, const T&); // 声明
template <typename U> U calc(const U&, const U&); // 声明
template <typename X> X calc(const X& a, const X& b) { // 定义
// ...
}
使用类的类型成员
如何通过类模板参数T,使用实例化之后T内定义的类型?
如果直接用作用域运算符(::)这样做,编译器无法判断是想使用T的静态成员value_type,还是想使用T内类型valuetype。
解决办法:通过typename显示告诉编译器,该名字是一个类型,而非static成员。
通知编译器一个名字表示类型时,只能用typename, 不能用class
// 声明类型的错误方式
T::value_type
// 声明类型的正确方式
template <typename T>
typename T::value_type top(const T& c) { // 注意这里的typename T::value表明这是一个类型
if(!c.empty()) return c.back();
else return typename T::value_type(); // 疑问:这里如果T::value_type表示类型,为何会带一个() ? 答案是这里的 类型+(),会调用默认构造函数(对类)或者内置的初始化方法(对内置类型,如int,初值一般为0)
};
默认模板实参
可以像指定函数默认实参一样,为模板参数提供默认实参。
template <typename T, typename F = less<T>> // F默认值是less<T>,一个模板类,重载了函数调用运算符(operator())
int compare(const T &v1, const T &v2, F f = F()) { // 这里F(),是相当于调用less<T>(),也就是less<T>的函数调用重载版本
if (f(v1, v2)) {
return -1;
}
if (f(v2, v1)) {
return 1;
}
return 0;
}
// 调用函数模板实例
auto i = compare(0, 42); // i = -1
注意:类模板同样也可以为类型参数,指定默认实参,也可以省略指定默认实参的部分。