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