类模板
本节将介绍类模板
初识类模板
类模板不是类,只有实例化类模板,编译器才能生成实际的类。
定义类模板
下面是一个类模板,它和普通类的区别只是多了一个 template<typename T>
template<typename T>
struct Test{};和函数模板一样,其实类模板的语法也就是:
template< 形参列表 > 类声明几乎所有我们前面讲的,函数模板中形参列表能写的东西,类模板都可以。
同样的,我们的类模板一样可以用 class 引入类型形参名,一样不能用 struct
template<class T>
struct Test{};使用类模板
下面展示了如何使用类模板 Test
template<typename T>
struct Test {};
int main(){
Test<void> t;
Test<int> t2;
//Test t; // Error!
}我们必须显式的指明类模板的类型实参,并且没有办法推导,事实上这个空类在这里本身没什么意义。
或许我们可以这样:
这理所应当,类模板能使用类模板形参,声明自己的成员,那么如何使用呢?
Test<void>我们稍微带入一下,模板的T是void那T t是?所以很合理Test t4{ 1 };C++17 增加了类模板实参推导,也就是说类模板也可以像函数模板一样被推导,而不需要显式的写明模板类型参数了,这里的Test被推导为Test<int>。
不单单是聚合体,当然,写构造函数也可以:
类模板参数推导
这涉及到一些非常复杂的规则,不过我们不用在意。
对于简单的类模板,通常可以普通的类似函数模板一样的自动推导,比如前面提到的 Test 类型,又或者下面:
new 表达式中一样可以。
同样的可以像函数模板那样加上许多的修饰:
多的就不用再提。
用户定义的推导指引
举个例子,我要让一个类模板,如果推导为 int,就让它实际成为 size_t:
如果要类模板 Test 推导为指针类型,就变成数组呢?
推导指引的语法还是简单的,如果只是涉及具体类型,那么只需要:
模板名称(类型a)->模板名称<想要让类型a被推导为的类型>
如果涉及的是一类类型,那么就需要加上 template,然后使用它的模板形参。
我们提一个稍微有点难度的需求:
类模板 array 同时使用了类型模板形参与常量模板形参,保有了一个成员是数组。
它无法被我们直接推导出类型,此时就需要我们自己定义推导指引。
这会用到我们之前在函数模板里学习到的形参包。
原理很简单,我们要给出 array 的模板类型,那么就让模板形参单独写一个 T 占位,放到形参列表中,并且写一个模板类型形参包用来处理任意个参数;获取 array 的 size 也很简单,直接使用 sizeof... 获取形参包的元素个数,然后再 +1 ,因为先前我们用了一个模板形参占位。
标准库的 std::array 的推导指引,原理和这个一样。
有默认实参的模板形参
和函数模板一样,类模板一样可以有默认实参。
必须达到 C++17 有 CTAD,才可以在全局、函数作用域声明为 X 这种形式,才能省略 <>。
但是在类中声明一个,有默认实参的类模板类型的数据成员(静态或非静态,是否类内定义都无所谓),不管是否达到 C++17,都不能省略 <>。
但是 gcc13.2 有不同行为,开启
std=c++17,类内定义的静态数据成员省略<>可以通过编译。但是,总而言之,不要类内声明中省略<>。
MinGw clang 16.02 与 msvc 均不可通过编译。
标准库中也经常使用默认实参:
当然了,也可以给常量模板形参以默认值,虽然不是很常见:
知道这些即可,这很合理,毕竟函数模板可以,你类模板也可以。
常量模板形参
前面其实已经提了,像 std::array 都是有常量模板形参的,这没有什么问题,类似于函数模板。
模板模板形参
类模板的模板类型形参可以接受一个类模板作为参数,我们将它称为:模板模板形参。
先随便给出一个简单的示例:
模板模板形参的语法略微有些复杂,我们需要理解一下,先把外层的 template<> 去掉。
template<typename T> typename C 我们分两部分看就好
前面的
template<typename T>就是我们要接受的类模板它的模板列表,是需要一模一样的,比如类模板 X 就是。后面的
typename是语法要求,需要声明这个模板模板形参的名字,可以自定义,这样就引入了一个模板模板形参。
下面是详细的语法形式:
可以有名字的模板模板形参。
有默认模板且可以有名字的模板模板形参。
可以有名字的模板模板形参包。
其实就是形参包的一种,能接受任意个数的类模板
当然了,模板模板形参也可以和常量模板形参一起使用,都是一样的,比如:
注意到了吗?我们省略了其中 template<std::size_t> 常量模板形参的名字,可能通常会写成 template<std::size_t N> ,我们只是为了表达这是可以省略了,看自己的需求。
对于普通的有形参包的类模板也都是同理:
成员函数模板
成员函数模板基本上和普通函数模板没多大区别,唯一需要注意的是,它大致有两类:
类模板中的成员函数模板
普通类中的成员函数模板
需要注意的是:
Class_template 的成员函数 f,它不是函数模板,它就是普通的成员函数,在类模板实例化为具体类型的时候,成员函数也被实例化为具体。
类模板中的成员函数模板
f就是成员函数模板,通常写起来和普通函数模板没多大区别,大部分也都 支持,比如形参包。普通类中的成员函数模板
f就是成员函数模板,没什么问题。
其实都是字面意思,很好理解,上面的示例都没什么实际的使用,都是语法展示,我相信明白函数模板就自然能明白这些。
可变参数类模板
形参包与包展开等知识,在类模板中是通用的。
std::tuple 是一个模板类,我们用来存储任意类型任意个数的参数,我们指明它的模板实参是使用的模板的类型形参包展开,std::tuple<Args...> 展开后成为 std::tuple<int,const char*,char,double> 。
构造函数中使用成员初始化列表来初始化成员 value,没什么问题,正常展开。
需要注意的是字符串字面量的类型是 const char[N] ,之所以被推导为 const char* 在于数组之间不能“拷贝”。它隐式转换为了指向数组首地址的指针,类型自然也被推导为 const char*。
类模板分文件
和前面提到的函数模板分文件的原因一样,类模板也没有办法分文件。
我们给出了一个项目示例,展示类模板通常分文件的情况。
通常就是统一写到 .h 文件中,或者大家约定俗成了一个 .hpp 后缀,这个通常用来放模板。
我们后面会单独做一个内容处理这些情况。
总结
类模板的知识远不止如此,不过目前也足够使用了,后续还会有补充。
我们写的类模板的内容没有函数模板那么多,主要在于很多内容是和函数模板重复的,很多特性彼此之间是相通的,我们就没必要讲那么多,所以需要注意,不要跳着看。
Last updated