模板和泛型编程 C++ primer笔记
16.1定义模板
重载多个相似的函数是麻烦的:
比如重载能接受多个类型的compare。
使用函数模板之后可以定义成这样:
template <typename T>
int compare(const T &v1, const T &v2)
{
if(v1 < v2) return -1;
if(v2 < v1) return 1;
return 0;
}
模板定义以关键字template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或者多个模板参数。
注:模板参数列表不能为空
使用模板时,我们显示或隐式的指定模板实参。
16.1.1函数模板
实例化函数模板
当我们调用一个函数模板时,编译器一般用函数实参来推断模板实参。
编译器用推断出的模板参数来为我们实例化一个特定版本的函数。
模板类型参数
在模板参数列表中,typename和class没有什么不同。
template<typename T, class U> calc (const T&, const U&)
非类型模板参数
除了定义类型参数,还可以在模板中定义非类型参数。
一个非类型参数表示一个值,而不是一个类型,我们使用特定的类型名,而不是关键字typename或者class.
可以用来指定数组大小。
template<unsigned N, unsigned M>
int compare(const char (&p)[N], const char (&p2)[N])
{
return strcmp(p1, p2);
}
当我们调用compare("hi","momom")时,编译器实例化出:
int compare(const char (&p1)[3], const char (&p2)[4])
绑定到非类型参数的实参必须是一个常量表达式。
函数模板可以声明为 inline 和 constexpr
模板编译:
因为我们调用一个类函数时,编译器只需要掌握函数的声明,所以一般将类定义和函数声明放到头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板的区别:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,模板的头文件通常包括声明和定义。
16.1.2类模板
使用类模板,需要提供显式的模板实参。
car
类模板的成员函数定义在类模板之外时,必须以template开始。
类模板的成员函数只有程序用到,才会进行实例化。
在类内简化模板类名的使用:类模板的作用域中,我们可以直接使用模板名,而不提供实参。
比如:BlobPtr& operator++
如果类包含友元的声明:
- 如果是非模板友元,友元被授权访问所有的模板实例。
- 如果是模板友元,可以只授权给特定实例。
template <typename T> class C2 {
friend class Pal<T>; //C2的每个实例将相同实例化的Pal声明为友元。
template <typename X> friend class Pal2; //Pal2的所有实例都是C2的每个实例的友元
friend class Pal3; //非模板类,是C2所有实例的友元。
};
static静态数据成员
直接定义static数据成员,则所有模板实例共享相同的静态变量。
也可以让实例有独有的static对象。
size_t Foo
当然访问的时候也要指定,访问的是那个静态变量。
16.1.3模板参数
如果希望通知编译器一个名字表示类型时,要使用关键字 typename
我们可以给模板提供默认模板实参
template
num<> A; // 使用默认实参也要有尖括号
类似函数默认实参,只有右侧所有参数都有默认实参是,它才能有默认实参。
16.1.4成员模板
一个普通的类可以包含本身是模板的成员函数,一样要在声明前面加 template...
对于类模板,成员和类也可以由自己,独立的模板。
在类模板外定义一个成员模板时,需要同时提供两个模板参数列表。
template <typename T> class Blob{
template <typename It> Blob(It b, It e);
}
template <typename T>
template <typename It>
Blob<T>::Blob(It b,It e)
16.1.5控制实例化
因为,相同的实例可以出现在多个对象文件中,进而存在在多个独立的编译生成文件中。这会带来很多额外的开销。
所以我们可以通过显示实例化来避免这种开销。
extern template class Blob<string>;
template int compare(const int&, const int&);
extern表明程序其他位置存在该实例化的非extern声明。
当编译器遇到extern声明时,它不会在本文件中生成实例化代码
16.2模板实参推断
从函数实参来确定模板实参的过程叫做模板实参推断
类型转换与模板类型参数
只有有限的几种自动转换可以用于模板参数:
- const转换
- 数组或函数指针转换
尾置返回值类型
尾置返回类型跟在形参列表后面并以一个 “->” 符号开头。
为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的位置放一个 auto:
auto getArrayType(int i) -> int(*)[10];
vector<int> vi = {1,2,3,4,5}
auto &i = fcn(vi.begin(), vi.end());
template <typename T>
auto fcn(T beg, T end) -> decltype(*beg)
{
return *beg;// 返回序列中一个元素的引用
}
当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
引用折叠:
- 所有的右值引用叠加到右值引用上还是右值引用
- 所有的其他引用类型之间的折叠都将变为左值引用
右值引用作为参数
template <typename T> void f3(T &&val)
{
T t = val;// 拷贝(右值) or 绑定引用(左值)
t = fcn(t); //赋值只改变t,还是既改变T也改变val
if(val == t){ } // 若T是左值引用类型,则一直为true
}
template <typename T> void f(T&&) //绑定到非const右值
template <typename T> void f(const T&) //绑定到const右值和左值
std::move
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t) // 显示类型转换
}
std::move的工作流程见书P611
remove_reference移除引用
可以使用static_cast进行显示类型转换,把左值转换为右值,不过还是使用move最好。
转发
某些函数需要将一个或者多个实参连同类型不变的转发给其他函数。(包括const 和 左右值特性)
template <typename F, typename T1,typename T2>
void flip1(F f, T1 t1, T2 t2) //顶层const和引用丢失了
{
f(t2,t1);
}
void f(int v1,int &v2) //注意v2是一个引用
{
cout << v1 " " << ++v2 << endl;
}
f(42,i);// f改变了实参i
flip1(f,j,42) //不会改变j
//所以改成
template <typename F, typename T1,typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) //可以传递左值引用,但不能接受f的参数有右值引用
{
f(t2,t1);
}
f(42,i);// f改变了实参i
flip2(f,j,42) //会改变j
//使用了forward
template <typename F, typename T1,typename T2>
void flip(F f, T1 &&t1, T2 &&t2) //可以传递左值引用,但不能接受f的参数有右值引用
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
forward可以保持实参类型的所有细节
16.3重载和模板
涉及函数模板的,函数匹配规则:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例
- 注意可以进行类型转换,但函数模板可以进行的类型转换很少
- 如果一个函数可以提供比其他函数更好的匹配则选择它,如果有多个同样的匹配:
- 如果只有一个非模板函数,则选择它
- 如果没有非模板函数,则选择更特例化的模板
- 否则该调用有歧义
注意要名字查找规则,声明不在前面就找不到,可能匹配到不需要的版本。
16.4可变参数模板
可变数目的参数被称为参数包,包括函数参数包和模板参数包。
template <typename T, typename... Args> //...用做扩展
void foo(const T &t,const Args& ... rest);
上面的代码表明了:foo是一个可变参数函数模板,它有一个名为T的类型参数,和一个Args的模板参数包。
foo的参数列表表示:包含一个const& 类型的参数,还有一个名为rest的函数参数包。
int i = 0; double d = 4.2; string s = "how"
foo(i,s,42,d);
生成:void foo(const int&,const string&,const int&,const double&)
可变参数函数通常是递归的。
sizeof...(Args) 可以返回包中含有多少元素。
包扩展
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
return print(os, debug_rep(rest)...);
}
扩展中的模式,会独立地应用在包中的每一个元素,上面的代码是对每个rest调用debug_rep,然后返回结果并作为函数参数。
可以结合forward保持原来参数的性质,和包扩展实现包转发。
return print(os, std::forward
16.5模板特例化
函数特例化
template <typename T> int compare(const T&, const T&); //通用模板
template<> //特例化模板
int compare(const char* const &p1, const char* const &p2)// T为const char* , 同时指定是常量指针而不是指向常量的指针
{
return strcmp(p1,p2);
}
特例化一个函数模板时,必须为原模版的每个模板参数提供实参。
在特例化中,我们提前实例化了模板,而非重载,所以特例化不影响函数匹配。
模板及其特例化版本应该声明在同一个头文件中。
类模板的特例化
类模板也可以特例化:
namespace std{
template <>
struct hash<Sales_data>//对标准库中hash模板进行特例化
{
typedef size_t result_type;
...
};
//如果使用了Sales_data的私有成员,要把std::hash声明为Sales_data的友元。
}
类模板的部分特例化
类模板可以进行部分特例化:
部分实例化版本的模板参数列表可以是原始模板参数列表的一个子集或者一个特例化版本。
template <class T> struct remove_reference
{
typedef T type;
}
template <class T> struct remove_reference<T&>
{
typedef T type;
}
template <class T> struct remove_reference<T&&>
{
typedef T type;
}
模板名后的<T&>是给特例化的模板参数指定实参,这些实参要和原始模板中的参数按位置对应。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本