OOP,能处理类型在程序运行之前都未知的情况;泛型编程,在编译时能获取类型。
模板是泛型编程的基础。本章学习如何定义自己的模板。
问题引出:假设希望编写一个函数来比较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; }
以形如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个重要原则:
但是,编写代码如果使用了 <, >运算符,就降低了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; }
模板编译*
编译器在模板定义时,不生成代码。只有实例化出模板的一个特定版本时,编译器才会生成代码。
函数模板和类模板成员函数的定义通常放在头文件中。
可以实例化出特定类的模板,叫类模板。
类模板是用来生成类的蓝图的。与函数模板的区别是,编译器不能为类模板推断模板参数类型。
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数据成员 // ... };
类似函数参数的名字,模板参数的名字只是一个符号,没有什么含义,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
注意:类模板同样也可以为类型参数,指定默认实参,也可以省略指定默认实参的部分。