16-模板和泛型编程
模板和泛型编程
- 面向对象的编程和泛型编程都能够处理在编写程序时,不清楚类型的情况;面向对象的编程处理的类型在程序运行之前都是未知的,泛型编程在程序编译时,就可以清楚类型的;
- 泛型编程是独立于任何特定类型来编码的,在使用一个泛型程序时,需要提供类型;
模板是泛型编程的基础
- 定义模板
* 1.函数模板就是一个通用的公式,用来针对特定的类型的函数版本;
`template <class T> int compare(const T &v1,const T &v2){ return v1 < v2 ? -1 : 1; }`
* 2.模板定义使用`template`开始,后面试模板参数列表,可以用于包含多个模板参数,不能为空,使用`<>`包围起来;
* 3.在模板调用时,调用者通过实参来初始化形参,模板参数表示的就是在类或者函数定义中使用的类型或者是值;
* 4.在使用模板时,编译器是可以通过形参类型来推断出模板参数来进行实例化模板类型参数;
`cout << compare(1,0) << endl; //可以自动推断出类型为int`
* 5.编译器生成的模板通常成为模板的实例化;
* 模板的类型参数表示的`T`,也就是接受形参的类型那个,返回类型是指返回值的类型,这两个之间没有必然的关系;
* 在类型参数前必须使用`class或者typename`,这两个参数没有明显的区别;比较建议使用`typename`,更为直观;
* 在模板中可以定义非类型参数,非类型参数可以表示一个值或者一个非类型,可以通过特定的类型名来指定非类型参数;
* 在一个模板被实例化时,非类型参数被一个用户提供的或者编译器推断出的值所代替,前提是这些值必须是常量表达式,编译器在编译时可以用来实例化模板;
* 非类型参数可以是一个整形,或者是一个指向对象或者函数类型的指针或者左值引用;
* 绑定到非类型整形参数的实参必须是一个常量表达式;
* 绑定到指针或者或引用非类型参数的实参必须具有静态的生存期;
* 不能够使用普通静态变量或者动态对象作为指针或引用非类型模板参数的实参;
* 其中指针参数可以使用`nullptr`或者一个值为`0`的常量表达式来实例化;
* 非类型模板参数的实参必须是常量表达式;
* 函数模板可以声明为`inline`或者`constexpr`的,声明格式:
template<typename T> inline T min(const T&,const T&);
* 泛型编程的两个重要原则:
* 1.模板中的参数是`const`的引用;
* 通过将参数设置为`const`的引用,可以保证函数可以用于不能拷贝的类型;
* 2.函数体中仅仅使用`<`运算符;
* 函数运算更快,对于运算符的要求更少;
* 通常来说内置类型和标准库类型`(除unique_ptr,IO类型)`都是允许进行拷贝;
* 上面的代码出现的问题是,如果用于调用两个指针,且两个指针指向相同的数组,则代码未定义;
template<typename T>
int compare(const T &v1,const T &v2){
if(less<T>()(v1,v2)) return -1;
if(less<T>()(v2,v1)) return 1;
return 0;
}
* 使用上面的方式可以巧妙的处理指针的问题;
* 编译器遇到模板定义时,并不会立即生成代码,只有实例化出模板的一个特定版本时,编译器才会生成代码.
* 通常情况下,当使用一个类类型对象时,类定义必须是可用的,但是成员函数的定义不必已经出现,应为成员函数还没有被使用,这样就可以将类定义函数声明,普通函数以及类的成员函数放在不同的文件里面;
* 对于模板来说:为了实例化一个版本编译器需要掌握函数模板或类模板成员函数的定义,模板的头文件通常需要包括声明也需要包括定义;
* 函数模板和类模板成员函数的定义通常放在头文件里面;
* 模板的分类:
* 不依赖于模板参数的名字,使用模板时,这些名字必须是可见的,模板被实例化时,模板的定义,类模板成员的定义也必须是可见的;
* 用来实例化模板的所有函数,类型以及与类型关联的运算符的声明都必须是可见的;
* 依赖于模板参数的名字;头文件里面应该包含:模板的定义,类模板或者成员定义中用到的所有名字的声明.模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件按;
* 模板实例化错误;
* 1.编译模板本身时,通常是语法错误检查,分号,变量名之类;
* 2.编译器遇到使用模板时,实参数目是否正确,参数类型是否匹配;
* 3.模板实例化时,类型相关的错误,错误最多的时候;
* 类模板
* 类模板使用来生成类的蓝图的,编译器不能够为类模板推断模板参数类型;
`template <typename T> class Blob`
* 实例化模板参数时,必须提供额外的参数类型信息,进行模板参数的显示实例化;
`Blob<int> ia; Blob<int> ia2 = {0,1,2,3,4};`
* 一个类模板的每个实例都形成一个独立的类,相互之间没有任何关联;
* 类模板的名字不是一个类型,类模板使用来实例化类型的,但是一个实例化的类型总是包含类模板参数的;
* 类模板的成员函数本身就是一个普通的函数,类模板的每个实例都有自己版本的成员函数.类模板的成员函数具有和模板相同的类模板参数,所以定义在类模板之外的成员函数必须以关键字`template`开头,后面接类模板参数列表.
`template <typename T> ret-type Blob<T>::member-name(parm-list)`
* 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化.
* 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被初始化;
* 在类模板内部使用类模板类型时,可以直接使用模板名,而不提供实参;
* 当在类模板作用域里面时,编译器处理自身引用时,是不需要提供实参的,因为自己可以得到,在类作用域的外部时,就需要使用类型名;
* 类和友元
* 当一个类包含一个友元声明时,类与友元各自是否是模板是无关的;
* 类模板和另一个类或者函数模板间友好关系的最常见形式是建立在对应实例及其对应友元间的友好关系.
* 为了引用类或者函数模板的一个特定的实例,我们必须先声明模板自身;
`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 Blob<T>; friend bool operator==<T> (const Blob<T>&,const Blob<T>&); };`
* 一个类可以将另一个模板的每个实例都声明为自己的友元函数,或者限定特定的实例为友元;这两种方式的实现是不一样的;
`template <typename T> class Pa1; class C { friend class Pa1<C>; template<typename T> friend class Pal1; }; template <typename T> class C2{ friend class Pa1<T>; template <typename X> friend class pa2; friend class Pal3; };`
* 为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数;
* 在`C++11`新标准中可以将模板参数声明为友元;
* 定义模板类型的方式
`template<typename T> using twin = pair<T,T>; twin<string> authors; ==>pair<string,string>;`
* 上面的过程表示的是一个模板类型别名就是一族类的别名;
* 类模板里面是可以声明`static`成员的;
* 类似任何其他成员函数,一个`static`成员函数只有在使用时才能够被初始化;
* 模板参数
* 模板参数的名字没有特殊的含义,但是模板参数列表中是不能够重名的;
* 模板参数遵从普通的作用域规则,一个模板参数名的可用范围是在其声明之后,直到模板声明或者定义之前.模板参数会隐藏外层作用中声明的相同名字,同样的在模板内部不能够使用模板参数名作为变量名;
* 模板声明必须包含模板参数;
`template <typename T> int compare(const T&,const T&); template <typename T> class Blob;`
* 一个特定文件所需要的所有模板的声明通常放在文件按的开始位置,出现在任何使用这些模板的代码之前;
* 当我们希望通知编译器一个名字表示类型时,必须使用关键字`typename`,而不是使用`class`;
* `C++11`新标准中可以为函数和类模板提供默认实参;
* 对于一个模板实参,只有当它的右侧的所有参数都有默认实参时,才可以又默认实参;
* 如果一个类模板为其所有的模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名后面跟一个`<>`对;
* 成员模板:
* 一个类可以包含本身是模板的成员函数,这种成员被称为成员模板,成员模板不能是虚函数;
* 对于类模板可以为其定义成员模板,类模板和成员模板有各自独立的模板参数,当在类模板外面定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表,类模板参数列表在前,后面跟成员自己的模板参数列表;
* 在那个对象上面调用成员模板,编译器会根据该对象的类型来推断类模板参数的实参,和普通构造函数相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参;
* 当独立编译多个多个源文件时,使用的是相同的模板,会造成额外的开销,可以通过显式实例化来避免开销;
`extern template declaration;//实例化声明; template declaration;//实例化定义; extern template class Blob<string>;//声明; template int compare(const int&,const int&);//定义;`
* 编译器在使用一个模板时,自动进行实例化,所以`extern`声明必须出现在任何使用实例化版本的代码之前;
* 当编译器遇到一个实例化定义时,它为其生成代码;
* 一个模板类实例化定义会实例化该模板的所有成员,包括内联的成员函数,因此我们显示实例化一个类模板的类型,必须能用于模板的所有成员;
* 在一个类模板的实例化定义中,所用类型必须能用于模板的所有成员函数.
* 两种灵活性:
* 通过两种不同的智能指针来分析绑定删除器时机的区别:
* 在运行时绑定删除器:
* `shared_ptr`不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才知道.但是实际上`shared_ptr`的删除器类型在生存周期内是可以随时改变的,类成员的类型在运行时是不能够改变的,因此不能够直接保存删除器,删除器是间接保存的;
* 在编译时绑定删除器:删除器类型是`unique_ptr`的一部分,删除器成员的类型在编译时是知道的,删除器可以直接保存在`unique_ptr`对象中;
* 在编译时绑定删除器,`unique_ptr`避免了间接调用删除器的运行时开销,通过在运行时绑定删除器,`shared_ptr`是用户重载删除器更为方便.
* 从函数实参来确定模板实参的过程被称为模板实参推断,模板实参推断的过程是:编译器使用函数调用中的实参类型来寻找模板实参,这些模板实参生成的函数版本与给定的函数调用最为匹配;
* 在进行类型转换的过程中,顶层`const`无论是形参中还是在实参中都会被忽略;
* 类型转换中,能在调用中应用于函数模板的两种情况:
* 1.`const`转换:可以将一个非`const`的引用或者指针传递给一个`const`的引用或者指针的形参;
* 2.数组或者函数指针转换:如果函数是餐不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换;一个数组实参可以转换为一个指向首元素的指针,一个函数实参可以转换为一个该函数类型的指针;
* 如果形参是引用,那么数组不会转换为指针;
* 在使用相同模板类型的实参,并且使用自动推倒时,如果两个参数的类型没有进行匹配,调用机会出错;
* 如果函数类型不是模板参数,则对实参进行正常的类型转换;
* 提供显示模板实参的方式与定义类模板实力的方式相同,显式模板实参在尖括号中给出,位于函数名之后,实参列表之前;
* 这两种转换是正常的:
* 普通类型定义的函数参数,允许进行正常的类型转换;
* 对于模板类型已经显式指定了的函数实参,也可以进行正常的类型转换;
* 尾置类型:
* 尾置返回出现在参数列表之后,它可以使用函数的参数:
template <typename It>
auto fcn(It beg, It end) ->decltype(*beg){
return *beg;
}
* 使用这种类型的引用方式可以返回`It&`的方式;
* 如果需要返回的是元素的拷贝:
template <typename It>
auto fcn2(It beg,It end) ->typename remove_reference<decltype(*beg)::type{
return *beg;
}
* 当使用一个函数模板初始化一个函数指针或者为一个函数指针赋值时,编译器使用指针类型来推断模板实参;
* 当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或者值;
* 从左值引用函数参数推断类型:当一个函数参数是模板类型参数的一个普通左值引用时,只能传递给其一个左值;
* 从右值引用函数参数推断类型:函数参数是右值`T&&`时,可以传递给其一个右值;
* 通常不能够将右值绑定到左值上面,两个例外:
* 1.影响右值引用参数的推断如何进行;
* 2.如果间接创建一个引用的引用,那么这些引用形成了折叠,所有情况下,饮用者叠成普通的左值引用类型.新标准中,折叠规则扩展到右值引用:只有在右值引用的右值引用.
* 引用折叠只能应用于间接创建的引用的引用,如类型别名或者模板参数;
* 如果实参是一个左值,则推断出的木耙参数类型将是一个左值引用,并且函数参数将被实例化为一个普通的引用参数;
* 如果一个函数是指向模板参数类型的右值引用,`(T&&)`,则可以传递给他俺任意类型的实参,如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用;
* 如果一个函数参数是一个指向模板类型参数的右值引用,那么就可以被绑定到一个左值;
* `std::move`
* `move` 是一个函数模板;
定义:
`template <typename T> typename remove_reference<T>::type&& move(T&& t){ return static_cast<typename remove_reference<T>::type&&>(t); }`
* 虽然不能够隐式的将一个左值转换为右值引用,但是我们可以用`static_cast`显式的将一个左值转换为一个右值引用;
* 转发
* 某些函数需要将一个或者多个实参连同类型不变的转发给其他函数,需要保证被转发实参的性质,实参类型是否是`const`的以及实参是左值还是右值;
* 可以通过将函数参数定义为一个指向模板类型参数的右值引用,可以保证对应实参的所有类型信息;
* 函数参数和其他任何变量一样,都是左值表达式;
* 当用于一个指向模板参数类型的右值引用函数参数`(T&&)`时,`forward`会保持实参类型的所有细节;
* 函数模板可以被另一个模板或一个普通非模板函数重载,名字相同的函数必须具有不同数量或类型的参数;
* 函数模板受到的限制:
* 1.对于一个引用,其候选函数包括所有模板实参推断成功的函数模板实例;
* 2.候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板;
* 3.可行函数`(模板和非模板)`按照类型进行排序;
* 4.如果有一个函数提供比任何其他函数更好的匹配,则选择此函数;
* 1.同样好的函数只有一个是非模板函数,则选择次函数;
* 2.如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个函数比其他模板更特例化,那么选择此模板;
* 当有多个重载版本对一个调用提供同样好的匹配时,应该选择最特例化的版本.
* 对于一个调用,如果费函数版本与一个函数版本提供同样好的匹配,则选择非模板版本;
* 使用一个没有声明的函数,代码编译会失败.
* 如果编译器可以从模板实例化出与调用匹配的版本,则缺少声明就不重要了;
* 在定义任何函数之前,记得声明所有重载的函数版本.
* 可变参数模板表示的是一个可以接受可变数目参数的模板函数或模板类,可变数目的参数被称为参数包.参数包包括:模板参数包:表示零个或者多个模板参数包;函数参数包:表示零个或多个函数参数;
template <typename T,typename... Args>
void foo(const T &t,const Args& ... rest);
* `sizeof`运算符可以用于查看类型参数的数目;
* 包扩展
* 1.当扩展一个包时,还需要提供每个扩展元素的模式.扩展一个包,就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表;
template <typename T,typename.. Args>
ostream &
print(ostream &os,const T &t,const Args&... rest){
os << t << ",";
return print(os,rest...);
}
* 模板特例化:
* 一个实例化的模板就是一个独立的定义,在其中一个或者多个模板参数被指定为特定的类型;
* 当我们特例化一个函数模板时,必须为元模板众的每个函数模板提供实参;
* 一个指针类型的`const`版本是一个常量指针而不是指向`const`类型的指针;
* 一个特例化本质上是一个实例,而不是函数名的一个重载版本;
* 特例化的本质是实例化一个模板,而不是重载它.因此,特例化不影响函数匹配;
* 将函数定义为特例化版本还是独立的非模板版本函数,会影响到函数匹配;
* 模板以及其特例化本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本;
* 类模板
* 类模板特例化不必为所有模板参数提供实参;
* 一个类模板的部分特例化本身就是一个模板,使用它时,用户必须为那些在特例化版本中未指定的模板参数提供实参;
* 我们只能部分特例化类模板,而不能部分特例化函数模板.
还没有评论,来说两句吧...