模板与泛型编程——定义模板
Posted acgame
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了模板与泛型编程——定义模板相关的知识,希望对你有一定的参考价值。
一、定义模板
1、函数模板
模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表,用<>括起来。在模板定义中,模板参数列表不能为空。模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 template<typename T> 9 int compare(const T &v1, const T &v2) { 10 if (v1 < v2) return -1; 11 if (v1 > v2) return 1; 12 return 0; 13 } 14 int main() 15 { 16 std::cout << compare(1, 2) << std::endl; 17 return 0; 18 }
1)实例化函数模板
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新“实例”。
2)模板类型参数
模板有模板类型参数,类型参数前必须使用关键字class或typename。一般来说,我们可以将类型参数看作类型说明符,就像内置类型或类类型说明符一样使用。特别是,类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
3)非类型模板参数
除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或typename来指定非类型参数。
当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用类型参数的实参必须具有静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用个nullptr或一个值为0的常量表达式来实例化。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 template<unsigned N, unsigned M> 9 int compare(const char(&p1)[N], const char(&p2)[M]) { 10 std::cout << N << "," << M << std::endl; 11 return strcmp(p1, p2); 12 } 13 int main() 14 { 15 compare("hi", "hello"); 16 return 0; 17 }
4)inline和constexpr的函数模板
inline或constexpr说明符放在模板参数列表之后,返回类型之前。
1 template<unsigned N, unsigned M> 2 inline int compare(const char(&p1)[N], const char(&p2)[M]) { 3 std::cout << N << "," << M << std::endl; 4 return strcmp(p1, p2); 5 }
5)模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出模板的一个特定版本时,编译器才会生成代码。当我们使用(而不是定义)模板时,编译器才生成代码,这一特性影响了我们如何组织代码以及错误何时被检测到。
通常,当我们调用一个函数时,编译器只需要掌握函数的声明。类似的,当我们使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,我们将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
函数模板和类模板成员函数的定义通常放在头文件中。
2、类模板
类模板是用来生成类的蓝图的。与函数模板的不同之处是,编译器不能为类模板推断模板参数类型。为了使用类模板,我们必须在模板名后的尖括号中提供额外信息——用来代替模板参数的模板实参列表。
1)定义类模板
1 #include <iostream> 2 #include <memory> 3 #include <stdexcept> 4 #include <string> 5 #include <vector> 6 7 template <typename T> 8 class Blob { 9 public: 10 typedef T value_type; 11 typedef typename std::vector<T>::size_type size_type; 12 13 Blob():data(std::make_shared<std::vector<T>>()){} 14 Blob(std::initializer_list<T> items):data(std::make_shared<std::vector<T>>(items)) {} 15 size_type size()const { return data->size(); } 16 bool empty()const { return data->empty(); } 17 void push_back(const T &t) { data->push_back(t); } 18 void push_back(T &&t) { data->push_back(std::move(t)); } 19 void pop_back() { 20 check(0, "pop_back error"); 21 data->pop_back(); 22 } 23 T &back() { 24 check(0, "back error"); 25 return data->back(); 26 } 27 T &operator[](size_type i) { 28 check(i, "[] error"); 29 return (*data)[i]; 30 } 31 32 private: 33 void check(size_type i, const std::string &msg)const { 34 if (i >= data->size()) 35 throw std::out_of_range(msg); 36 } 37 private: 38 std::shared_ptr<std::vector<T>> data; 39 }; 40 int main() 41 { 42 Blob<int> b({ 1,2,3 }); 43 std::cout << b[1] << std::endl; 44 return 0; 45 }
2)实例化类模板
当使用一个类模板时,必须提供额外信息,这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
3)类模板的成员函数
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
类模板的每个实例都有其自己版本的成员函数。因此,类模板的成员函数具有和模板相同的模板参数。因而,定义在类模板之外的成员函数就必须以关键字template开始,后接类模板参数。
1 template <typename T> 2 class Blob { 3 public: 4 typedef T value_type; 5 typedef typename std::vector<T>::size_type size_type; 6 7 Blob():data(std::make_shared<std::vector<T>>()){} 8 Blob(std::initializer_list<T> items):data(std::make_shared<std::vector<T>>(items)) {} 9 size_type size()const { return data->size(); } 10 bool empty()const { return data->empty(); } 11 void push_back(const T &t) { data->push_back(t); } 12 void push_back(T &&t) { data->push_back(std::move(t)); } 13 void pop_back() { 14 check(0, "pop_back error"); 15 data->pop_back(); 16 } 17 T &back() { 18 check(0, "back error"); 19 return data->back(); 20 } 21 T &operator[](size_type i) { 22 check(i, "[] error"); 23 return (*data)[i]; 24 } 25 26 private: 27 void check(size_type i, const std::string &msg)const; 28 private: 29 std::shared_ptr<std::vector<T>> data; 30 }; 31 template<typename T> 32 void Blob<T>::check(size_type i, const std::string &msg)const { 33 if (i >= data->size()) 34 throw std::out_of_range(msg); 35 }
4)类模板成员实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。
5)在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参,但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
1 template <typename T> 2 class Blob { 3 public: 4 typedef T value_type; 5 typedef typename std::vector<T>::size_type size_type; 6 7 Blob():data(std::make_shared<std::vector<T>>()), curr(0){} 8 Blob(std::initializer_list<T> items) 9 :data(std::make_shared<std::vector<T>>(items)), curr(0) {} 10 void show() { 11 std::cout << curr << std::endl; 12 } 13 Blob &operator++(); // 前置运算符 14 15 private: 16 void check(size_type i, const std::string &msg)const; 17 private: 18 std::shared_ptr<std::vector<T>> data; 19 std::size_t curr; 20 }; 21 template<typename T> 22 void Blob<T>::check(size_type i, const std::string &msg)const { 23 if (i >= data->size()) 24 throw std::out_of_range(msg); 25 } 26 27 template<typename T> 28 Blob<T> &Blob<T>::operator++() { 29 ++curr; 30 return *this; 31 }
当我们处于一个类模板的作用域中时,编译器处理模板自身时就好像我们提供了与模板参数匹配的实参一样。
6)在类模板外使用类模板名
当我们在类模板外定义其成员时,必须记住,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域。
由于返回类型位于类的作用域之外,我们必须指出返回类型是一个模板,它所用类型与类实例化所用类型一致。在函数体内,我们已经进入类的作用域,因此无须重复模板实参。如果不提供模板实参,则编译器将假定我们使用的类型与成员实例化所用类型一致。
1 template <typename T> 2 class Blob { 3 public: 4 typedef T value_type; 5 typedef typename std::vector<T>::size_type size_type; 6 7 Blob():data(std::make_shared<std::vector<T>>()), curr(0){} 8 Blob(std::initializer_list<T> items) 9 :data(std::make_shared<std::vector<T>>(items)), curr(0) {} 10 void show() { 11 std::cout << curr << std::endl; 12 } 13 Blob &operator++(); // 前置运算符 14 Blob operator++(int); 15 16 private: 17 void check(size_type i, const std::string &msg)const; 18 private: 19 std::shared_ptr<std::vector<T>> data; 20 std::size_t curr; 21 }; 22 template<typename T> 23 void Blob<T>::check(size_type i, const std::string &msg)const { 24 if (i >= data->size()) 25 throw std::out_of_range(msg); 26 } 27 28 template<typename T> 29 Blob<T> &Blob<T>::operator++() { 30 ++curr; 31 return *this; 32 } 33 34 template<typename T> 35 Blob<T> Blob<T>::operator++(int) { 36 Blob ret = *this; 37 ++*this; 38 return ret; 39 }
7)类模板和友元
当一个类包含一个友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
8)一对一友好关系
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友元关系。
1 #include <iostream> 2 #include <memory> 3 #include <stdexcept> 4 #include <string> 5 #include <vector> 6 7 template<typename T> class BlobPtr; 8 template<typename T> class Blob; 9 template<typename T> 10 bool operator==(const Blob<T> &, const Blob<T> &); 11 12 template<typename T> 13 class Blob { 14 friend class Blob<T>; 15 friend bool operator==<T>(const Blob<T> &, const Blob<T> &); 16 public: 17 Blob(T _x) :x(_x) {} 18 private: 19 T x; 20 }; 21 template<typename T> 22 bool operator==(const Blob<T> &lhs, const Blob<T> &rhs) { 23 std::cout << lhs.x << ", " << rhs.x << std::endl; 24 return lhs.x == rhs.x; 25 } 26 int main() 27 { 28 Blob<std::string> a("abc"), b("ab"); 29 std::cout << (a == b) << std::endl; 30 return 0; 31 }
友元的声明用Blob的模板形参作为它们自己的模板实参。因此,友元关系被限定在用相同类型实例化的Blob与BlobPtr、相等运算符之间。
9)通用和特定模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者特定限定的实例为友元:
1 template<typename T> class Pal; 2 class C { 3 // 用类C实例化的Pal是C的一个友元 4 friend class Pal<C>; 5 // Pal2的所有实例都是C的友元;这种情况无须前置声明 6 template<typename T> friend class Pal2; 7 }; 8 9 template<typename T> 10 class D { 11 // D的每个实例将相同实例化的Pal声明为友元,Pal的模板声明必须在作用域之内 12 friend class Pal<T>; 13 // Pal2的所有实例都是D的每个实例的友元,不需要前置声明 14 template<typename X> friend class Pal2; 15 // Pal3是D的每个实例的友元,不需要Pal3的前置声明 16 friend class Pal3; 17 };
10)令模板自己的类型参数成为友元
在新标准中,我们可以将模板类型参数声明为友元:
1 template <typename Type> 2 class Bar { 3 friend Type; 4 };
11)模板类型别名
与任何其他类相同,类模板可以声明static成员。模板类的每个static数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static对象。因此,与定义模板的成员函数类似,我们将static数据成员也定义为模板。类似任何其他成员函数,一个static成员函数只有在使用时才会实例化。
1 #include <iostream> 2 #include <memory> 3 #include <stdexcept> 4 #include <string> 5 #include <vector> 6 7 template<typename T> 8 class Foo { 9 public: 10 static std::size_t count() { return ctr; } 11 Foo() { 12 ++ctr; 13 } 14 private: 15 static std::size_t ctr; 16 }; 17 template<typename T> 18 std::size_t Foo<T>::ctr = 0; 19 int main() 20 { 21 Foo<int> a, b; 22 std::cout << Foo<int>::count() << std::endl; 23 Foo<std::string> c; 24 std::cout << Foo<std::string>::count() << std::endl; 25 return 0; 26 }
3、模板参数
1)模板参数与作用域
模板参数遵循普通的作用域规则。有一个模板参数名的可用范围在其声明之后,至模板声明或定义结束之前。与任何其他名字一样,模板参数会隐藏外层作用域中声明的相同名字。但是,与大多数其他上下文不同,在模板内不能重用模板参数名:
1 typedef double A; 2 template <typename A, typename B> 3 void f(A a, B b) { 4 A tmp = a; // tmp的类型为模板参数A的类型,而非double 5 double B; // 错误:重用模板参数名 6 }
由于参数名不能重用,所以一个模板参数名在一个特定模板参数列表中只能出现一次。
2)模板声明
模板声明必须包含模板参数。声明中的模板参数的名字不必与 定义中相同。一个给定模板的每个声明和定义必须具有相同数量和种类(即,类型和非类型)的参数。
1 template<typename T> 2 int compare(const T&, const T&); 3 template<typename T> 4 class Blob;
3)使用类的类型成员
默认情况下,C++语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显示告诉编译器该名字是一个类型。我们只能通过使用关键字typename来实现这一点。
1 template<typename T> 2 typename T::value_type top(const T& c) { 3 if (!c.empty()) 4 return c.back(); 5 else 6 return typename T::value_type(); 7 }
4)默认模板实参
我们可以提供默认模板实参。在新版标准中,我们可以为函数和类模板提供默认实参。与函数默认实参一样,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
5)模板默认实参和类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。尖括号指出类必须从一个模板实例化出来。特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个尖括号对。
4、成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板。成员模板不能是虚函数。
1)非模板类的成员模板
1 class Demo { 2 public: 3 template <typename T> 4 void show(T x) { 5 std::cout << x << std::endl; 6 } 7 };
2)类模板的成员模板
对于类模板,我们也可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数。
与类模板的普通成员函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表。
1 #include <iostream> 2 #include <memory> 3 #include <stdexcept> 4 #include <string> 5 #include <vector> 6 7 template <typename T> 8 class Blob { 9 public: 10 template <typename X> 11 void show(X); 12 }; 13 template <typename T> 14 template <typename X> 15 void Blob<T>::show(X x) { 16 std::cout << x << std::endl; 17 } 18 int main() 19 { 20 Blob<std::string> b; 21 b.show(233); 22 return 0; 23 }
3)实例化与成员模板
为了实例化一个类模板的成员模板,我们必须同时提供类和函数模板的实参。与往常一样,我们在哪个对象上调用成员模板,编译器就根据该对象的类型来推断类模板参数的实参。与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参。
5、控制实例化
当模板被使用时才会进行实例化这一特性意味着,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就会有该模板的一个实例。
在大系统中,在多个文件实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化有如下形式:
extern template declaration; // 实例化声明
template declaration; // 实例化定义
declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参。
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定版本的实例化版本,可能有多个extern声明,但必须只有一个定义。
1 #pragma once 2 #include <iostream> 3 4 template <typename T> 5 class Blob { 6 public: 7 void show(const T &x) { 8 std::cout << __FUNCTION__ << ", " << x << std::endl; 9 } 10 }; 11 12 template <typename T> 13 bool compare(const T &lhs, const T &rhs) { 14 return lhs == rhs; 15 }
1 #include "TemplateBuild.h" 2 template class Blob<int>; 3 template bool compare(const int&, const int&);
1 #include <iostream> 2 #include <memory> 3 #include <stdexcept> 4 #include <string> 5 #include <vector> 6 #include "header/TemplateBuild.h" 7 8 extern template class Blob<int>; 9 extern template bool compare(const int&, const int&); 10 int main() 11 { 12 Blob<int>().show(233); 13 std::cout << compare(2, 3) << std::endl; 14 return 0; 15 }
1)实例化定义会实例化所有成员
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。当编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数。因此,与处理类模板的普通实例化不同,编译器会实例化该类的所有成员。即使我们不使用某个成员,它也会被实例化。因此,我们用来显式实例化一个类模板的类型,必须能用于模板的所有成员。
以上是关于模板与泛型编程——定义模板的主要内容,如果未能解决你的问题,请参考以下文章