🎉作者简介:👓 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢 c + + , g o , p y t h o n , 目前熟悉 c + + , g o 语言,数据库,网络编程,了解分布式等相关内容 \textcolor{orange}{博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容} 博主在读机器人研究生,目前研一。对计算机后端感兴趣,喜欢c++,go,python,目前熟悉c++,go语言,数据库,网络编程,了解分布式等相关内容
📃 个人主页: \textcolor{gray}{个人主页:} 个人主页: 小呆鸟_coding
🔎 支持 : \textcolor{gray}{支持:} 支持: 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦 \textcolor{green}{如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦} 如果觉得博主的文章还不错或者您用得到的话,可以免费的关注一下博主,如果三连收藏支持就更好啦👍 就是给予我最大的支持! \textcolor{green}{就是给予我最大的支持!} 就是给予我最大的支持!🎁
💛本文摘要💛
本专栏主要是对c++ primer这本圣经的总结,以及每章的相关笔记。目前正在复习这本书。同时希望能够帮助大家一起,学完这本书。 本文主要讲解第16章 模板与泛型编程 c++ primer 第五版 系列文章:可面试可复习
第2章 变量和基本类型
第3章 字符串、向量和数组
第4章 表达式
第5章 语句
第6章 函数
第8章 IO库
第9章 顺序容器
第10章 泛型算法
第11章 关联容器
第12章 动态内存
第13章 拷贝控制
第 14章 重载运算符
第15章 面向对象程序设计
第 16章 模板与泛型编程
🎆模板与泛型编程
-
面向对象编程
和泛型编程
都能处理在编写程序时不知道类型的情况- OOP 能处理类型在程序运行之前都未知的情况。
- 泛型编程
在编译时
就能知道类型了。容器、迭代器、泛型算法都是泛型编程的例子。模板是泛型编程的基础
,一个模板就是一个创建类或函数的蓝图。
🎅16.1 定义模板
🎄16.1.1 函数模板
-
模板定义以关键字 template 开始,后跟一个用 <> 包围,用逗号分隔的模板参数列表。
-
在模板定义中,模板参数列表不能为空
-
模板参数列表类似函数列表,需要在类或函数定义中用到的类型和值。使用模板时,需要指定模板实参,将其绑定到模板参数上。
'普通函数'
int compare(const string &v1, const string &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
'模板'
template <typename T>
bool compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
实例化函数模板
- 调用函数模板时,
编译器用函数实参来推断模板实参,然后实例化出一个特定版本的函数。
//实例化出int compare(const int &, const int &)
cout << compare(1, 0) << endl; //T为int
//实例化int compare(const vector<int> &, const vector<int> &)
vector<int> vec1{1, 2, 3}, vec{4, 5, 6};
模板类型参数
- 模板类型参数当做内置类型或类类型一样使用
类型参数可以用来指定返回类型或函数的参数类型
模板参数前必须使用关键字 class 或 typename,两个含义相同。
'正确'
template <typename T, class U> calc(const T&, const U&);
'错误'
template <typename T, U> calc(const T&, const U&);
//T参数返回类型, T* p参数类型
template <typename T> T foo(T* p)
{
T tmp = *p
return *p;
}
'实例'
int a = 0;
int foo(int *p)
{
int tmp = *p;
return tmp;
}
cout << foo(& a) << endl;
非类型模板参数
- 可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。
- 通过一个特定的类型名
(如int, double等)而非关键字class或typename来指定非类型参数
当一个模板实例化时,非类型参数被用户提供的值所代替,这些值必须是常量表达式,以允许编译器在编译时实例化模板。
- 非类型参数可以是一个
整型或指针或引用
。绑定到指针或引用非类型参数的实参必须具有静态的生存期。 非类型模板参数的模板实参必须是常量表达式(例如指定数组大小)
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}//定义了一个函数模板。
compare("hi", "mom");//实例化
'实例化的版本'
int compare(const char (&p1)[3], const char (&p2)[M]);
//编译器会在一个字符串字面值常量的末尾插入一个空字符作为终结符
-
注意
一般对于非类型模板参数,限制const此时可以传递const和非const
inline 和 constexpr 的函数模板
- 函数模板可以声明为 inline 或 constexpr 的。
与普通函数不同,inline和constexpr说明符放在模板参数列表之后,返回类型之前
template<typename T> inline T main(const T&, const T&);
编写类型无关的代码
-
编写泛型代码有两个重要原则:
-
模板中的函数参数应该是 const 的引用
。引用保证了函数可以用于不能拷贝的类型,如unique_ptr, IO 类型。 函数体中的条件判断仅使用 < 比较运算。
-
//缺点:对类型要求比较高,如果调用它比较俩个指针,且指针未指向相同的数组,则会出错
if (v1 < v2) return -1;
if (v1 > v1) return 1;
return ;
//传递指针也正确
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;
}
- 注意:
- 模板程序应该尽量减少对实参类型要求
模板编译
-
编译器遇到模板定义时不生成代码,当实例化出模板的一个特定版本时才生成代码
。这会影响错误何时被检测到。 - 定义类时,普通的成员函数一般放在源文件中。
但是模板不同,模板的头文件通常既包括声明也包括定义
。因为编译器需要知道函数模板或类模板成员函数的完整定义才能进行实例化。
大多数编译错误在实例化期间报告
- 第一阶段:编译模板本身时。只能检测到语法错误。
- 第二阶段:编译器遇到模板使用时。只能检测模板实参是否与形参相匹配。
- 第三阶段:模板实例化时。这时才能发现类型相关的错误。
🎄16.1.2 类模板
- 类模板与函数模板不同,
编译器不能为类模板推断模板参数类型
。通过在模板名后的尖括号中提供额外信息,来代替模板参数的模板实参列表
定义类模板
template <typename T> class Blob {}
实例化类模板
- 当使用一个类模板时,必须提供额外的信息,也就是
显示模板实参列表
,他们被绑定到模板参数
Blob<int> ia; //使用类模板
template <> class Blob<int> {}; // 当向上一行那样使用类模板时,编译器实例化出的类就是这样的。
- 编译器从Blob模板实例化出一个类时,他会重写Blob类,将模板参数T的每个实例,替换为给定的模板实参,上面就全部替换成了
int类型
注意
- 一个类模板的每一个实例都能形成一个独立的类。例如
Blob<string>与Blob<double>
类型没有关联,也不会有特殊的访问权限,这完全就是俩个独立的Blob类
//定义的实例化出俩个不同的Blob类型
Blob<string> names;
Blob<double> prices;
类模板的成员函数
可以在类模板内部,也可以在外部定义成员函数。定义在类模板内部的函数隐式声明为内联函数。
定义在类模板之外的成员函数必须以关键字 template 开始,后接类模板参数列表。
'格式'
template <typename T>
ret_type Blob<T>::member_name(parm-list)
template <typename T> void Blob<T>::check(){}
templte <typename T> T& Blob<T>::operator[]{}
//Blob<T>::在外部定义成员函数是,必须说明成员属于哪一个类
//模板实参与模板形参必须相同
类模板成员函数的实例化
默认情况下,一个类模板的成员函数只有当用到它时才进行实例化。
- 如果一个成员函数没有被使用,则它不会实例化。
在类代码内简化模板类名的使用
使用一个类模板类型时必须提供模板实参,在一个类模板的作用域内,可以直接使用模板而不败指定模板实参
在类模板外使用类模板名必须提供模板实参。
template <typename T> class BlobPtr{
BlobPtr<T>& operator++();
BlobPtr<T>& operator--();
};
'在类的作用域(内部)可以省略'
template <typename T> class BlobPtr{
BlobPtr& operator++();
BlobPtr& operator--();
};
在类模板外使用类模板名
- 当在类模板定义其成员时,必须记住,
我们并不在类的作用域中,直到遇到类名才表示进入类的作用域
类模板和友元
如果一个类模板包含一个非模板友元,则该友元可以访问该模板的所有实例。
如果友元也是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
//每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
template <typename T> class Blob{
'友元的T和上面的T类型必须一致'
friend class BlobPtr<T>; // 每个 Blob 实例将访问权限授予了同类型实例化的 BlobPtr。
friend bool operator==<T> (const Blob<T>&,const Blob<T>&); // 将访问权限授予同类型实例化的 ==。
}
'友元的声明用Blob的模板形参作为他们自己的模板实参'
'因此友好关系被限制在相同类型实例化的Blob和BlobPtr之间'
Blob<char> ca; // BlobPtr<char>和operator==<char> 都是本对象的友元
Blob<int> ia; // BlobPtr<int>和operator==<int> 都是本对象的友元
'注意:'
BlobPtr<char> 的成员可以访问ca的非public部分,但是ca对ia或其他Blob的任何实例没有特殊访问权限
通用和特定的模板友好关系
- 为了让所有实例都成为友元,友元声明中必须使用与类模板不同的模板参数。
template <typename T> class Blob{
template <typename X> friend class Pal; // Pal 的所有实例都是 Blob 的每个实例的友元。
}
令模板自己的类型参数成为友元
template <typename T> class Blob{
friend T; // 将访问权限授予用来实例化 Blob 的类型
}
模板类型别名
可以定义一个 typedef 来引用实例化的类,但不能引用模板
typedef Blob<string> StrBlob;//正确
可以用 using 为类模板定义类型别名
template <typename T> using twins = pair<T, T>; // 为 pair<T, T> 定义了一个类型别名 twins
twins<string> authors; // authors 是一个 pair<string,string>。
'可以固定一个或多个模板参数,上面只是固定一个'
template <typename T> using partNo = pair<T, unsigned>;
partNo<string>book; //pair<string, unsigned>
定义模板类型别名时,可以固定其中的部分模板参数
template <typename T> using twins = pair<T, unsigned>; // 为 pair<T, unsigned> 定义了一个类型别名 twins
twins<string> authors; // authors 是一个 pair<string, unsigned>。
类模板的 static 成员
- 如果类模板定义了 static 成员,那么模板的每个实例都有自己独有的 static 成员实例。
static 数据成员定义时也要定义为模板
template <typename T> int Blob<T>::num = 0; // 在类模板外定义 static 数据成员的方式。
- 可以通过域访问符::访问里面静态成员,使用时才会被实例化,也可以通过对象访问,
- 但是不能通过模板访问,因为成员必须有个具体的类才行。
🎄16.1.3 模板参数
- 一个模板参数的名字没有实际意义,既可以写T,也可以写其他的名字
模板参数与作用域
- 模板参数的可用范围是其声明之后,至模板声明或定义结束之前。
模板参数会隐藏外层作用域中的相同名字,但是注意在模板内不能重用模板参数名。
一个模板参数名在一个特定模板参数列表中只能出现一次
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; //tmp的类型为模板参数A的类型,而非double
double B; //错误:重声明模板参数B
}
模板声明
模板声明必须包含模板参数,声明中的模板参数的名字不必与定义中相同(与函数形参类似)。
'3个calc都指向相同的函数模板'
template <typename T> T calc (const T &, const T&); //声明
template <typename U> U calc (const U&, const T&) //声明
template <typename Type> Type calc(const Type& a, const Type& b){....} //定义
- 一个文件所需要的所有模板的声明通常都一起放置在文件的开始位置。
使用类的类型成员
- 默认情况下,C++ 假定通过作用域运算符访问的名字不是类型(比如可能是静态成员)。
如果希望使用一个模板类型参数的类型成员,必须使用关键字 typename 显式地告诉编译器该名字是一个类型,而不能使用class
template <typename T>
typename T::value_type top (const T&c)
{
....
}
默认模板实参
可以为函数模板和类模板的模板参数提供默认模板实参,就像可以为函数参数提供默认实参一样。
//compare有一个默认模板实参less<>和一个默认函数实参F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F())
{......}
模板默认实参与类模板
- 如果一个类模板为其所有模板参数都提供了默认实参,且希望使用默认实参,必须在模板名后面跟一个空尖括号对:
🎄16.1.4 成员模板
- 一个类(不管是普通类还是类模板)都可以包含本省是模板的成员函数。
- 成员模板(member template):本身是模板的函数成员。
- 普通(非模板)类的成员模板。
- 类模板的成员模板。
普通(非模板)类的成员模板
注意:成员模板不能是虚函数
类模板的成员模板
- 可以对类模板,定义其成员模板,此时类模板与成员模板都是独立的模板
- 与类模板的
普通函数成员
不同的是, 成员模板是函模板。当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
template<typename T> class Blob{
template<typename it> Blob(lt b, lt e);
....
};
template<typename T> //类的类型参数
template<typename ll> //构造函数的类型参数
Blob<T>::Blob(lt b, lt e);
data(std:make_shared<std::vector<T>>(b, e)){}
实例化与成员模板
- 实例化一个类模板的成员模板,必须同时提供类和函数模板的实参
- 俩步走:
- 首先根据对象的类型推断出类模板的参数的实参
- 然后根据传递给成员模板的函数实参来推断它的模板实参
第一步:定义a1时,显示的指出编译器应该实例化一个int版本Blob
第二步:构造函数自己的类型参数通过begin(a1)和end(a1)类型推断
🎄16.1.5 控制实例化
-
产生控制实例化原因:
- 当模板被使用时才会进行实例化,而每个模板的源码都单独的放在一个文件中,当进行单独编译是,就会出现重复(在多个文件中实例化相同模板的额外开销可能非常严重。)
-
显示实例化:
-
extern template declaration; // 实例化声明
-
template declaration; // 实例化定义
extern template class Blob<string>; //声明
template int compare(const int &, const int &); //定义
🎄16.1.6 效率与灵活性
🎅16.2 模板实参推断
编译器通常不是对实参进行类型转换,他不知道到怎么转换,而是生成一个新的模板实例
🎄16.2.1 类型转换与模板类型参数
- 通常不会进行类型转换,但是有特例
-
const转换
:可以将一个非const对象的引用(指针)传递给一个const的引用(或指针)形参 -
数组或函数指针转换
:如果函数形参不是引用类型,则可以将数组或函数转换为相应的指针。(数组实参转换为一个指向其首元素的指针,函数实参转换为指向一个该函数类型的指针)
使用相同 模板参数类型的函数形参
- 一个模板类型参数可以用作多个函数形参的类型。但是只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型
- 如果推断出类型不匹配,只能传递俩个类型参数
long lng;
compare(lng, 1024); //错误:不能实例化compare(lng, 1024);类型不匹配且不能转换
template<typename A, typename B>
int flexibleCompare(const A& v1, const B& v2){....};
long lng;
flexibleCompare(lng, 1024); //正确但是有一个条件,这里面俩个类型是兼容的可以进行比较。
正常类型转换应用于普通函数实参
- 函数模板可以用普通类型定义的参数,这种函数可以进行进行形参的转换
(对于模板类型,只允许几种情况进行自动转换,intL类型不可以转换,但是对于普通类型来说,可以进行转换)
🎄16.2.2 函数模板显示实参
- 一般情况编译器无法推断模板实参类型,此时需要对他指定显示模板实参
指定显示模板实参
'只有实例化后编译器才能推导出类型'
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
'显示模板实参'
auto val3 = sum<long long>(i, lng); // T1是显式指定,T2和T3都是从函数实参类型推断而来
-
当需要给定T3类型时,T1和T2都要提前给,否则会出错,所以对于这种糟糕的设计,三个类型都要显示给出
正常类型转换应用于显示指定的实参
- 对于普通类型定义的函数参数可以进行正常的转换,同样对于模板类型参数已经
显示指定了的函数实参
,也可以正常的类型转换
🎄16.2.3 尾置返回类型与类型转换
- 当确定返回类型时,可以显示模板实参表示模板函数的返回类型,但是这样会增加额外负担。
-
decltype(*lt)``lt
既不是迭代器对象,也不是指针对象,只是一个类型,因此不可以这样使用 -
decltype(*beg)
,可以这样使用,但是在编译器遇到函数的列表之前,beg都是不存在的 - 可以使用
尾置返回类型
,由于尾置返回出现在参数列表之后,他可以使用函数的参数
🎄16.2.4 函数指针和实参推断
当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
- 当参数是一个函数模板实例的地址时,程序上下文必须满足,对于每个模板参数能唯一确定其类型或值
🎄16.2.5 模板实参推断和引用
从左值引用函数参数推断类型
从右值引用函数参数推断类型
- 从右值引用推导,这样就可以接收右值
template<typename T> void f3(T&&)
f3(42); //实参是一个int类型的右值;模板参数T是int
引用折叠和右值引用参数
- 引用折叠只能应用于间接创建引用的引用,如类型别名或模板参数
- 引用的引用是不存在的,但是现在有了,只能进行引用折叠
只有俩个都是右值引用才能折叠成右值引用
- 右值引用既可以接收左值,也可以接收右值
编写接收右值引用参数的模板函数
template<typename T> void f3(T && val)
{
T t = val; //拷贝还是绑定一个引用
t = fcn(t); //赋值只改变t还是既改变t又改变val;
if(val == t); //若T是引用类型,则一直是true
}
'对一个右值调用f3时,例如字面值常量42,T为int'
'对一个左值i调用f3时,T为int &,此时t和val绑定在一起','结果一直为true'
- 在实际中,右值引用通常用于俩种情况:
模板转发其实参
模板被重载
'目前使用右值引用的函数模板通常使用481页的方式重载'
template <typename T> void f(T&&); //绑定到非const右值
template <typename T> void f(const T&); //左值和const 右值
🎄16.2.6 理解std:move
move函数可以将左值引用变为右值引用(虽然不能直接将一个右值引用绑定到一个左值,但是用move获得绑定到左值上的右值引用)
std::move是如何定义的
std::move是如何工作的
🎄16.2.7 转发
- 某些函数需要将一个或多个实参连同类型不变地转发给其他函数。因此需要保持转发实参的所有性质,包括实参类型是否是
const
的以及实参是左值还是右值 - 使用一个名为
forward
的新标准库设施来传递参数,它能够保持原始实参的类型。 - 定义在头文件
utility
中。 - 必须通过显式模板实参来调用。
-
forward
返回显式实参类型的右值引用。即,forward<T>
的返回类型是T&&
。
🎅16.3 重载与模板
- 函数模板可以被另一个模板或一个普通非模板函数重载。与普通函数重载一样,名字相同的函数必须具有不同数量或类型的参数
编写重载模板
int main()
{
string s("hi");
'模板一更精确'
cout << debug_rep(s) << endl; //调用第一个模板,T类型为string
'模板二更精确'
cout << debug_rep(&s) << endl; //地址可以被封装成指针,
//debug_rep(cosnt string*&),由第一个版本的debug_rep实例化而来,T被绑定到string *
//debug_rep( string*),由第二个版本的debug_rep实例化而来,T被绑定到string
'无法确定,但是根据规定,选择最特化版本'
const string *sp = &s;
cout << debug_rep(sp) << endl; //都是精确匹配,但是第二个更特化
//debug_rep(cosnt string*&),由第一个版本的debug_rep实例化而来,T被绑定到string *
//debug_rep(cosnt string*),由第二个版本的debug_rep实例化而来,T被绑定到const string
}
非模板和模板重载文章来源:https://www.toymoban.com/news/detail-828054.html
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
🎅16.4 可变参数模板
可变参数模板就是一个接受可变数目参数的模板函数或模板类。文章来源地址https://www.toymoban.com/news/detail-828054.html
- 可变数目的参数被称为参数包。
- 模板参数包:标识另个或多个模板参数。
- 函数参数包:标识另个或者多个函数参数。
- 用一个省略号来指出一个模板参数或函数参数,表示一个包。
-
template <typename T, typename... Args>
,Args
第一个模板参数包。 -
void foo(const T &t, const Args& ... rest);
,rest
是一个函数参数包。 -
sizeof...
运算符,返回参数的数目。
🎄16.4.1 编写可变参数函数模板
- 可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
🎄16.4.2 包扩展
- 对于一个参数包,除了获取它的大小,唯一能做的事情就是扩展(expand)。
- 扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。
🎄16.4.3 转发参数包
- 新标准下可以组合使用可变参数模板和
forward
机制,实现将实参不变地传递给其他函数。
🎅16.5 模板特例化
- 定义函数模板特例化:关键字
template
后面跟一个空尖括号对(<>)。 - 特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
- 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
- 我们可以部分特例化类模板,但不能部分特例化函数模板。
到了这里,关于【c++ primer 笔记】第 16章 模板与泛型编程的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!