C++Primer5th 第十六章 模板与泛型编程

    技术2022-07-11  81

    第十六章 模板与泛型编程 16.1 定义模板 16.1.1 函数模板 实例化函数模板模板类型参数非类型模板参数inline和constexpr的函数模板编写类型无关的代码模板编译模板大多数编译错误在实例化期间报告 16.1.2 类模板 定义类模板实例化类模板类模板的成员函数类模板成员函数的实例化类代码内简化模板类名的使用类模板和友元一对一友好关系通用和特定的模板友好关系模板类型别名类模板的static成员 16.1.3 模板参数 模板参数与作用域模板声明使用类的类型成员默认模板实参 16.1.4 成员模板 普通类的成员模板类模板的成员函数实例化与成员模板 16.1.5 控制实例化16.1.6 效率与灵活性 16.2 模板实参推断 16.2.1 类型转换与模板类型参数 使用相同模板参数类型的函数形参正常类型转换应用于普通函数实参 16.2.2 函数模板显式实参 指定显式模板实参正常类型转换应用于显式指定的实参 16.2.3 尾置返回类型与类型转换 进行类型转换的标准库模板类 16.2.4 函数指针和实参推断16.2.5 模板实参推断和引用 从左值引用函数参数推断类型从右值引用函数参数推断类型引用折叠和右值引用参数 16.2.6 理解std::move16.2.7 转发 16.3 重载与模板16.4 可变参数模板 sizeof…运算符16.4.1 编写可变参数函数模板16.4.2 包扩展16.4.3 转发参数包 16.5 模板特例化 定义函数模板特例化函数重载与模板特例化类模板特例化类模板部分特例化特例化成员而不是类

    第十六章 模板与泛型编程

    OOP和泛型编程能够处理不知道类型写程序,区别是:前者能处理程序运行之前都是未知的情况;后者在编译时就能获知类型

    16.1 定义模板

    16.1.1 函数模板

    template<typename T> template关键字后加模板参数列表,逗号分隔的一个或多个模板参数的列表,模板参数列表不能为空

    模板参数表示在类或函数定义中用到的类型或值,使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。编译时候根据情况确定T的实际类型

    实例化函数模板

    当我们调用函数模板时,编译器用函数实参来为我们推断模板实参,编译器用推断出的模板参数来为我们实例化,这些编译器生成的版本通常称为模板的实例。

    模板类型参数

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

    非类型模板参数

    模板中可以定义非类型参数,非类型参数表示值而不是类型,通过类型名指定非类型参数,其被用户提供的或是编译器推断的值代替,这些值必须是常量表达式。

    用处:在需要常量表达式的地方,可以用非类型参数,比如指定数组的大小。

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

    inline和constexpr的函数模板

    放置在模板参数列表之后,返回类型之前

    编写类型无关的代码

    编写泛型代码的两个重要原则:

    模板中函数参数是const函数体条件判断仅使用小于比较运算

    模板编译

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

    当我们调用一个函数时,编译器只需要直到函数的声明,类似的,使用类类型时,类定义必须是可用的,但成员函数的定义不必已经出现,因此通常,将类定义和函数的声明放在头文件中,普通函数和成员函数的定义放在源文件中。

    模板则不同,为了生成实例化版本,编译器需要直到函数模板或类模板成员函数的定义,因此,模板的头文件通常包含声明和定义。

    模板大多数编译错误在实例化期间报告

    编程者要保证传递给模板的实参支持模板所要求的操作,这些操作在模板中能够正确工作

    16.1.2 类模板

    类模板用于生成类的蓝图,但是与函数模板不同之处是编译器不能帮类模板推断模板参数类型,需要用户在尖括号中指定

    定义类模板

    template <typename T> class ClassName {...}

    实例化类模板

    使用类模板时候,提供额外信息——显式模板实参列表 ClassName<int> v 一个类模板的每个实例都是都形成一个独立的类

    类模板的成员函数

    类模板成员函数具有和模板相同的模板参数,定义在类模板之外的成员函数必须以template开始,后接类模板参数列表

    template <typename T> ret-type ClassName<T>::member-name(parm-list)

    类模板成员函数的实例化

    默认情况,一个类模板的成员函数只有程序用到它时才进行实例化。

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

    在类模板的作用域内,可以直接使用模板名字而不提供实参。

    在类模板外使用类模板名直到遇到类名才表示进入类的作用域

    类模板和友元

    一个类包含一个友元,类与友元各自是否是模板是相互无关的:

    如果一个类包含一个非模板友元,则友元被授权访问所有模板实例如果友元也是模板,则类可以授权给所有友元模板实例或特定实例

    一对一友好关系

    类模板与另一个(类或函数)模板间的友好关系的最常见的形式是建立对应实例及其友元间的友好关系。

    通用和特定的模板友好关系

    一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元。

    为了让所有实例称为友元,友元声明中必须使用与类模板本身不同的模板参数。

    模板类型别名

    可以用typedef来引用实例化的类 typedef Blob<string> StrBlob;

    可以为类模板定义一个类型别名:

    template <typename T> using twin = pair<T, T>; twin<string> a; // a 是 pair<string, string> //可以固定一个或多个模板参数 template <typename T> using partNo = pair<T, int>; partNo<string> c; //c是 pair<string, int>

    类模板的static成员

    对于任意给定的类型,都有一个对应类型的静态成员,所有相同类型的对象共享相同的静态对象。

    16.1.3 模板参数

    通常将类型参数命名为T,但是实际上可以使用任何名字

    模板参数与作用域

    遵循普通的作用域规则

    一个模板参数名的可用范围是在其声明之后至模板声明或定义结束之前。

    模板参数会隐藏外层作用域中声明相同的名字,模板内不能重用模板参数名

    一个模板参数名在一个特定模板参数列表中只能出现一次。

    模板声明

    模板声明必须包含模板参数,声明中模板参数的名字和定义中的名字可不同,类型数量必须一样。

    使用类的类型成员

    C++默认情况假定通过作用域运算符访问的是名字而不是类型,对于模板代码,如果希望使用类型参数的类型成员,就要显示告知编译器,使用typename

    tpyename T::value_type top(const T& c); //T::value_type是T的类型成员而不是一个静态成员

    为了让编译器能够知道作用域访问的名字是类型还是static成员,对于非模板代码,编译器掌握类的定义,因此直到通过作用域运算符访问的名字是类型还是静态成员

    默认模板实参

    可以提供默认模板实参,必须在模板参数列表最后 如果一个类模板为其所有模板参数提供了默认实参,且我们希望使用默认实参,则在模板名后价格空尖括号对。

    16.1.4 成员模板

    一个类(无论是普通还是类模板)可以包含本身是模板的成员函数,这种成员被称为成员模板,其不能是虚函数

    普通类的成员模板

    与任何函数模板相同

    类模板的成员函数

    类和成员有各自独立的模板参数,在类模板外定义一个成员模板时候,需要同时为类模板和成员模板提供模板参数列表,类模板参数列表在前,成员模板参数列表在后。

    实例化与成员模板

    为了实例化类模板的成员模板,需要同时提供类和函数的模板的实参。

    16.1.5 控制实例化

    当模板被使用时才会进行实例化,为了避免相同实例出现在多个文件中,减少开销,我们通过显式实例化来避免开销对于给定的实例化版本,可以有多个声明,但是只能有一个定义。 extern template declaration; //实例化声明 template declaration; //实例化定义 extern template class Blob<string>; //声明 template in compare(int, int); //定义

    对于每个实例化声明,程序中某个未知必须有显式的实例化定义

    实例化定义会实例化所有成员,所用类型必须能够用于模板的所有成员函数

    16.1.6 效率与灵活性



    16.2 模板实参推断

    从函数实参来确定模板实参的过程被称为模板实参推断.

    16.2.1 类型转换与模板类型参数

    通常编译器不是对实参进行类型转换,而是生成一个新的模板实例,但对于const转换 和 数组和函数指针转换可以进行.

    使用相同模板参数类型的函数形参

    一个模板类型参数可以给做个函数作为形参,由于只有有限的几种类型转换,因此传递的实参必须类型相同,如果希望不相同,那就分配不同的形参给函数

    正常类型转换应用于普通函数实参

    如果函数参数类型不是模板参数,则对实参进行正常的类型转换.

    16.2.2 函数模板显式实参

    有些情况编译器无法推断出模板实参类型,也有情况,希望用户控制模板实例化

    指定显式模板实参

    template <typename T1,typename T2, typename T3 > T1 sum(T2, T3);

    没有函数实参类型推断T1, 因此调用sum时候需要为T1提供显式模板实参

    //指定了第一个参数类型,剩下两个由实参推断 auto a = sum<long long >sum(i, j);

    显式模板实参按由左至右的顺序与对应的模板参数匹配,只有最右参数的显式模板实参可以忽略

    正常类型转换应用于显式指定的实参

    对于模板类型参数已经显式指定了的函数实参,可进行正常的类型转换

    16.2.3 尾置返回类型与类型转换

    如果可以通过函数参数推断出返回类型,那么需要返回类型出现在参数列表之后,因此使用尾置返回类型.

    //返回类型与*beg一致,可以用decltype template <typename It> auto f(It beg, It end) -> decltype(*beg) { return *beg; }

    进行类型转换的标准库模板类

    上述代码返回的是引用类型,迭代器操作只能生成元素的引用,不能生成元素,为了获得元素,可以使用标准库的类型转换模板,定义在头文件type_traits中

    template <typename It> auto f2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type { return *beg; }

    remove_reference脱去引用

    16.2.4 函数指针和实参推断

    当一个函数模板初始化一个函数指针或为函数指针赋值,编译器使用指针的类型推断模板实参

    当参数是一个函数模板实例的地址时,程序上下文对每个模板参数,能唯一确定其类型或值

    16.2.5 模板实参推断和引用

    template <typename T> void f(T& p);参数p是模板类型参数T的引用,记住两个重要点:

    编译器会应用正常的引用绑定规则;const是底层的,不是顶层的;

    从左值引用函数参数推断类型

    当一个函数参数是模板类型参数的一个左值引用时(T&),只能传递给它一个左值如果函数参数类型是const T& , 可以传递给其任何类型实参

    从右值引用函数参数推断类型

    T&& , 传递给它一个右值,可推断出T的类型是右值实参的类型

    引用折叠和右值引用参数

    通常右值引用不能绑定到左值上,但是C++定义了两个例外规则,这也是move这种标准库工作的基础

    第一个例外:将左值给函数的右值引用参数,且右值引用指向模板类型参数(T&&)时,编译器推断模板类型参数为实参的的左值引用类型(T&),这看起来时定义了引用的引用,但是不知直接定义引用的引用,可以通过类型别名或通过模板参数类型间接定义.第二个例外:间接创建了引用的引用会发生折叠,引用会折叠成一个普通的左值引用类型,只有在右值引用的右值引用情况下,折叠为右值引用. X& &, X& && , X&& & 折叠为X&X&& &&折叠为X&&

    - 引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数

    16.2.6 理解std::move

    tmeplate <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }

    代码很妙!:

    传递给其右值,string&& move(string&& t)传递给其左值,string&& move(string& t)

    可以用static_cast 将左值变为右值,截断左值

    16.2.7 转发

    某些函数需要将其一个或多个实参连同类型不变地转发给其他函数,保持实参的所有性质,包括是否是const,左值还是右值

    如果一个函数参数是指向模板类型参数的右值引用,它对应的实参的const属性和左值/右值属性将保持

    当用于一个指向模板参数类型的右值引用函数参数(T&&)时,std::forward会保持实参类型的所有细节.



    16.3 重载与模板

    函数模板可以被另一个模板或一个普通非模板函数重载,名字相同的函数必须具有不同数量或类型的参数

    当有多个重载模板对一个调用提供同样好的匹配时候,应选择最特例化的版本.

    关于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本.定义任何函数之前,声明所有重载的函数版本,避免导致调用的函数并不是我们理想的版本

    16.4 可变参数模板

    可变参数模板: 接收可变数目参数的模板函数或模板类,可变数目的参数称为参数包

    模板参数包: 表示零个或多个模板参数;函数参数包: 表示零个或多个函数参数.使用省略号…来指出一个模板参数或函数参数表示一个包.typename…或class…指出接下来的参数表示零个或多个类型的列表一个类型名后边跟一个省略号表示零个或多个给定类型的非类型参数的列表.

    sizeof…运算符

    template <typename ... Args> void g(Args... args) { cout << sizeof...(Args); //类型数目 cout << sizeof...(args);// 参数数目 }

    16.4.1 编写可变参数函数模板

    可变参数函数通常是递归的

    第一步调用处理包中的第一个实参,然后剩余实参调用自身.为了终止递归,还需要定义一个非可变参数的函数,否则无限递归

    16.4.2 包扩展

    一个参数包,我们可以获取其大小,还可以扩展,扩展一个包还需要为它提供用于每个扩展元素的模式

    扩展一个包就是将其分解为构成的元素,对每一个元素应用模式,获得扩展后的列表,通过在模式右边放一个省略号…触发扩展操作

    16.4.3 转发参数包

    结合使用可变参数模板和std::forward机制来编写函数,实现将其实参不变地传给其他函数.



    16.5 模板特例化

    当我们不能或不希望使用模板版本时,可以定义类或函数模板的一个特例化版本, 一个特例化版本就是模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型.

    定义函数模板特例化

    特例化一个函数模板必须为每个模板参数都提供实参. 为了指出我们正在实例化一个模板,应该是用关键字template后跟一个空尖括号<>

    函数重载与模板特例化

    函数模板的特例化本质上时我们接管了编译器的工作,为原模版的一个特殊实例提供了定义.

    特例化的本质时实例化一个模板,而不是重载它,特例化不影响函数匹配

    模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本

    类模板特例化

    namespace std { //打开std命名空间 } //关闭命名空间,无分号

    为了指出我们正在实例化一个模板,应该是用关键字template后跟一个空尖括号<>

    类模板部分特例化

    我们只能部分特例化类模板,不能部分特例化函数模板.

    与函数模板不同,类模板的特例化不必为所有模板参数提供实参,可以只制定一部份模板参数

    一个类模板的部分特例化本身是一个模板.

    特例化成员而不是类

    可以只特例化特定成员函数而不是特例化整个模板.

    template<typename T> struct Foo{ void Bar(); //... } //特例化成员 template<> void Foo<int>::Bar(){...}
    Processed: 0.012, SQL: 9